mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-09-24 15:00:31 +08:00
refactor: roadmap specific guides (#9043)
* fix: ai course generate url * wip * wip * wip * feat: roadmap guides * wip * wip * feat: featured guide list
This commit is contained in:
@@ -1,22 +1,18 @@
|
||||
import type { GuideFileType } from '../../lib/guide';
|
||||
import type { QuestionGroupType } from '../../lib/question-group';
|
||||
import type { OfficialGuideDocument } from '../../queries/official-guide';
|
||||
import { GuideListItem } from './GuideListItem';
|
||||
|
||||
export interface FeaturedGuidesProps {
|
||||
heading: string;
|
||||
guides: GuideFileType[];
|
||||
questions: QuestionGroupType[];
|
||||
guides: OfficialGuideDocument[];
|
||||
questions: OfficialGuideDocument[];
|
||||
}
|
||||
|
||||
export function FeaturedGuideList(props: FeaturedGuidesProps) {
|
||||
const { heading, guides, questions = [] } = props;
|
||||
|
||||
const sortedGuides: (QuestionGroupType | GuideFileType)[] = [
|
||||
...guides,
|
||||
...questions,
|
||||
].sort((a, b) => {
|
||||
const aDate = new Date(a.frontmatter.date as string);
|
||||
const bDate = new Date(b.frontmatter.date as string);
|
||||
const sortedGuides = [...guides, ...questions].sort((a, b) => {
|
||||
const aDate = new Date(a.publishedAt ?? new Date());
|
||||
const bDate = new Date(b.publishedAt ?? new Date());
|
||||
|
||||
return bDate.getTime() - aDate.getTime();
|
||||
});
|
||||
@@ -27,7 +23,7 @@ export function FeaturedGuideList(props: FeaturedGuidesProps) {
|
||||
|
||||
<div className="mt-3 sm:my-5">
|
||||
{sortedGuides.map((guide) => (
|
||||
<GuideListItem key={guide.id} guide={guide} />
|
||||
<GuideListItem key={guide._id} guide={guide} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -48,4 +44,4 @@ export function FeaturedGuideList(props: FeaturedGuidesProps) {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +1,46 @@
|
||||
import type { GuideFileType, GuideFrontmatter } from '../../lib/guide';
|
||||
import { type QuestionGroupType } from '../../lib/question-group';
|
||||
import dayjs from 'dayjs';
|
||||
import { DateTime } from 'luxon';
|
||||
import {
|
||||
getOfficialGuideHref,
|
||||
type OfficialGuideDocument,
|
||||
} from '../../queries/official-guide';
|
||||
|
||||
export interface GuideListItemProps {
|
||||
guide: GuideFileType | QuestionGroupType;
|
||||
}
|
||||
|
||||
function isQuestionGroupType(
|
||||
guide: GuideFileType | QuestionGroupType,
|
||||
): guide is QuestionGroupType {
|
||||
return (guide as QuestionGroupType).questions !== undefined;
|
||||
guide: OfficialGuideDocument;
|
||||
}
|
||||
|
||||
export function GuideListItem(props: GuideListItemProps) {
|
||||
const { guide } = props;
|
||||
const { frontmatter, id } = guide;
|
||||
const { title, slug, publishedAt, roadmapId } = guide;
|
||||
|
||||
let pageUrl = '';
|
||||
let guideType = '';
|
||||
|
||||
if (isQuestionGroupType(guide)) {
|
||||
pageUrl = `/questions/${id}`;
|
||||
guideType = 'Questions';
|
||||
} else {
|
||||
const excludedBySlug = (frontmatter as GuideFrontmatter).excludedBySlug;
|
||||
pageUrl = excludedBySlug ? excludedBySlug : `/guides/${id}`;
|
||||
guideType = (frontmatter as GuideFrontmatter).type;
|
||||
let guideType = 'Textual';
|
||||
if (roadmapId === 'questions') {
|
||||
guideType = 'Question';
|
||||
}
|
||||
|
||||
// Check if article is within the last 15 days
|
||||
const isNew = frontmatter.date
|
||||
? dayjs().diff(dayjs(frontmatter.date), 'day') < 15
|
||||
: false;
|
||||
const publishedAtDate = publishedAt
|
||||
? DateTime.fromJSDate(new Date(publishedAt))
|
||||
: null;
|
||||
|
||||
const isNew =
|
||||
publishedAtDate && DateTime.now().diff(publishedAtDate, 'days').days < 15;
|
||||
const publishedAtMonth = publishedAtDate
|
||||
? publishedAtDate.toFormat('MMMM')
|
||||
: '';
|
||||
|
||||
return (
|
||||
<a
|
||||
className="text-md group block flex items-center justify-between border-b py-2 text-gray-600 no-underline hover:text-blue-600"
|
||||
href={pageUrl}
|
||||
className="text-md group flex items-center justify-between border-b py-2 text-gray-600 no-underline hover:text-blue-600"
|
||||
href={getOfficialGuideHref(slug, roadmapId)}
|
||||
>
|
||||
<span className="text-sm transition-transform group-hover:translate-x-2 md:text-base">
|
||||
{frontmatter.title}
|
||||
{title}
|
||||
|
||||
{isNew && (
|
||||
<span className="ml-2.5 rounded-xs bg-green-300 px-1.5 py-0.5 text-xs font-medium text-green-900 uppercase">
|
||||
New
|
||||
<span className="hidden sm:inline">
|
||||
·
|
||||
{frontmatter.date ? dayjs(frontmatter.date).format('MMMM') : ''}
|
||||
{publishedAtMonth}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
---
|
||||
import { getGuideTableOfContent, type GuideFileType } from '../../lib/guide';
|
||||
import MarkdownFile from '../MarkdownFile.astro';
|
||||
import { TableOfContent } from '../TableOfContent/TableOfContent';
|
||||
import { RelatedGuides } from './RelatedGuides';
|
||||
|
||||
interface Props {
|
||||
guide: GuideFileType;
|
||||
}
|
||||
|
||||
const { guide } = Astro.props;
|
||||
|
||||
const allHeadings = guide.getHeadings();
|
||||
const tableOfContent = getGuideTableOfContent(allHeadings);
|
||||
|
||||
const showTableOfContent = tableOfContent.length > 0;
|
||||
const showRelatedGuides =
|
||||
guide?.relatedGuides && Object.keys(guide?.relatedGuides).length > 0;
|
||||
const { frontmatter: guideFrontmatter, author } = guide;
|
||||
---
|
||||
|
||||
<article class='lg:grid lg:max-w-full lg:grid-cols-[1fr_minmax(0,700px)_1fr]'>
|
||||
{
|
||||
(showTableOfContent || showRelatedGuides) && (
|
||||
<div class='sticky top-0 lg:relative bg-linear-to-r from-gray-50 py-0 lg:col-start-3 lg:col-end-4 lg:row-start-1'>
|
||||
<RelatedGuides
|
||||
relatedTitle={guideFrontmatter?.relatedTitle}
|
||||
relatedGuides={guide?.relatedGuides || {}}
|
||||
client:load
|
||||
/>
|
||||
<TableOfContent toc={tableOfContent} client:load />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div
|
||||
class:list={[
|
||||
'col-start-2 col-end-3 row-start-1 mx-auto max-w-[700px] py-5 sm:py-10',
|
||||
{
|
||||
'lg:border-r': showTableOfContent,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<MarkdownFile>
|
||||
<h1 class='mb-3 text-balance text-4xl font-bold'>
|
||||
{guideFrontmatter.title}
|
||||
</h1>
|
||||
<p class='my-0 flex items-center justify-start text-sm text-gray-400'>
|
||||
<a
|
||||
href={`/authors/${author.id}`}
|
||||
class='inline-flex items-center font-medium underline-offset-2 hover:text-gray-600 hover:underline'
|
||||
>
|
||||
<img
|
||||
alt={author.frontmatter.name}
|
||||
src={author.frontmatter.imageUrl}
|
||||
class='mb-0 mr-2 inline h-5 w-5 rounded-full'
|
||||
/>
|
||||
{author.frontmatter.name}
|
||||
</a>
|
||||
<span class='mx-2 hidden sm:inline'>·</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`}
|
||||
target='_blank'
|
||||
>
|
||||
Improve this Guide
|
||||
</a>
|
||||
</p>
|
||||
<guide.Content />
|
||||
</MarkdownFile>
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,60 @@
|
||||
import { cn } from '../../lib/classname';
|
||||
import { guideRenderer } from '../../lib/guide-renderer';
|
||||
import type { OfficialGuideResponse } from '../../queries/official-guide';
|
||||
import { TableOfContent } from '../TableOfContent/TableOfContent';
|
||||
import { RelatedGuides } from './RelatedGuides';
|
||||
|
||||
type GuideContentProps = {
|
||||
guide: OfficialGuideResponse;
|
||||
};
|
||||
|
||||
export function GuideContent(props: GuideContentProps) {
|
||||
const { guide } = props;
|
||||
const content = guideRenderer.render(guide.content);
|
||||
const tableOfContents = guideRenderer.tableOfContents(guide.content);
|
||||
const showTableOfContent = tableOfContents.length > 0;
|
||||
const hasRelatedGuides =
|
||||
guide.relatedGuides && guide.relatedGuides.length > 0;
|
||||
|
||||
return (
|
||||
<article className="lg:grid lg:max-w-full lg:grid-cols-[1fr_minmax(0,700px)_1fr]">
|
||||
{(showTableOfContent || hasRelatedGuides) && (
|
||||
<div className="sticky top-0 bg-linear-to-r from-gray-50 py-0 lg:relative lg:col-start-3 lg:col-end-4 lg:row-start-1">
|
||||
{hasRelatedGuides && (
|
||||
<RelatedGuides relatedGuides={guide?.relatedGuides || []} />
|
||||
)}
|
||||
|
||||
{showTableOfContent && <TableOfContent toc={tableOfContents} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'col-start-2 col-end-3 row-start-1 mx-auto max-w-[700px] py-5 sm:py-10',
|
||||
showTableOfContent && 'lg:border-r',
|
||||
)}
|
||||
>
|
||||
<div className="prose prose-xl prose-h2:mb-3 prose-h2:mt-10 prose-h2:scroll-mt-5 prose-h2:text-balance prose-h2:text-3xl prose-h3:mt-2 prose-h4:text-2xl prose-h3:scroll-mt-5 prose-h3:text-balance prose-h4:text-balance prose-h5:text-balance prose-h5:font-medium prose-blockquote:font-normal prose-code:bg-transparent prose-img:mt-1 sm:prose-h2:scroll-mt-10 sm:prose-h3:scroll-mt-10 prose-li:[&>p]:m-0 container">
|
||||
<h1 className="mb-3 text-4xl font-bold text-balance">
|
||||
{guide.title}
|
||||
</h1>
|
||||
<p className="my-0 mb-6 flex items-center justify-start text-sm text-gray-400">
|
||||
<a
|
||||
href={`/authors/${guide.author?.slug}`}
|
||||
className="inline-flex items-center font-medium underline-offset-2 hover:text-gray-600 hover:underline"
|
||||
>
|
||||
<img
|
||||
alt={guide.author?.name}
|
||||
src={guide.author?.avatar}
|
||||
className="mr-2 mb-0 inline h-5 w-5 rounded-full"
|
||||
/>
|
||||
{guide.author?.name}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import type { OfficialGuideDocument } from '../../queries/official-guide';
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
type RelatedGuidesProps = {
|
||||
relatedTitle?: string;
|
||||
relatedGuides: Record<string, string>;
|
||||
relatedGuides: Pick<OfficialGuideDocument, 'title' | 'slug' | 'roadmapId'>[];
|
||||
};
|
||||
|
||||
export function RelatedGuides(props: RelatedGuidesProps) {
|
||||
@@ -12,14 +13,7 @@ export function RelatedGuides(props: RelatedGuidesProps) {
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const relatedGuidesArray = Object.entries(relatedGuides).map(
|
||||
([title, url]) => ({
|
||||
title,
|
||||
url,
|
||||
}),
|
||||
);
|
||||
|
||||
if (relatedGuidesArray.length === 0) {
|
||||
if (relatedGuides.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -47,23 +41,32 @@ export function RelatedGuides(props: RelatedGuidesProps) {
|
||||
isOpen && 'block',
|
||||
)}
|
||||
>
|
||||
{relatedGuidesArray.map((relatedGuide) => (
|
||||
<li key={relatedGuide.url}>
|
||||
<a
|
||||
href={relatedGuide.url}
|
||||
className="text-sm text-gray-500 no-underline hover:text-black max-lg:block max-lg:border-b max-lg:px-3 max-lg:py-1"
|
||||
onClick={() => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
{relatedGuides.map((relatedGuide) => {
|
||||
const { roadmapId, slug, title } = relatedGuide;
|
||||
const href = roadmapId ? `/${roadmapId}/${slug}` : `/guides/${slug}`;
|
||||
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{relatedGuide.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
const className = cn(
|
||||
'text-sm text-gray-500 no-underline hover:text-black max-lg:block max-lg:border-b max-lg:px-3 max-lg:py-1',
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={slug}>
|
||||
<a
|
||||
href={href}
|
||||
className={className}
|
||||
onClick={() => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Fragment, useEffect, useRef, useState } from 'react';
|
||||
import type { QuestionType } from '../../lib/question-group';
|
||||
import { markdownToHtml } from '../../lib/markdown';
|
||||
import Prism from 'prismjs';
|
||||
import './PrismAtom.css';
|
||||
import { guideRenderer, type QuestionType } from '../../lib/guide-renderer';
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
type QuestionCardProps = {
|
||||
question: QuestionType;
|
||||
@@ -20,8 +18,6 @@ export function QuestionCard(props: QuestionCardProps) {
|
||||
// width if the answer is visible and the question height is less than
|
||||
// the answer height
|
||||
if (isAnswerVisible) {
|
||||
Prism.highlightAll();
|
||||
|
||||
const answerHeight = answerRef.current?.clientHeight || 0;
|
||||
const questionHeight = questionRef.current?.clientHeight || 0;
|
||||
|
||||
@@ -69,7 +65,7 @@ export function QuestionCard(props: QuestionCardProps) {
|
||||
</div>
|
||||
|
||||
<div className="mx-auto flex max-w-[550px] flex-1 items-center justify-center py-3 sm:py-8">
|
||||
<p className="px-4 text-xl font-semibold leading-snug! text-black sm:text-3xl">
|
||||
<p className="px-4 text-xl leading-snug! font-semibold text-black sm:text-3xl">
|
||||
{question.question}
|
||||
</p>
|
||||
</div>
|
||||
@@ -88,27 +84,15 @@ export function QuestionCard(props: QuestionCardProps) {
|
||||
|
||||
<div
|
||||
ref={answerRef}
|
||||
className={`absolute left-0 right-0 flex flex-col items-center justify-center rounded-[7px] bg-neutral-100 py-4 text-sm leading-normal text-black transition-all duration-300 sm:py-8 sm:text-xl ${
|
||||
isAnswerVisible ? 'top-0 min-h-[248px] sm:min-h-[398px]' : 'top-full'
|
||||
}`}
|
||||
className={cn(
|
||||
'absolute right-0 left-0 flex flex-col items-center justify-center rounded-[7px] bg-neutral-100 py-4 text-sm leading-normal text-black transition-all duration-300 sm:py-8 sm:text-xl',
|
||||
isAnswerVisible ? 'top-0 min-h-[248px] sm:min-h-[398px]' : 'top-full',
|
||||
)}
|
||||
>
|
||||
{!question.isLongAnswer && (
|
||||
<div
|
||||
className={`mx-auto flex max-w-[600px] grow flex-col items-center justify-center py-0 px-5 text-center text-base [&>p]:leading-relaxed sm:text-xl`}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: markdownToHtml(question.answer, false),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="qa-answer prose prose-h5:font-semibold prose-h5:mb-2 prose-h5:text-black prose-sm prose-quoteless prose-h1:mb-2.5 prose-h1:mt-7 prose-h2:mb-3 prose-h2:mt-0 prose-h3:mb-[5px] prose-h3:mt-[10px] prose-p:mb-2 prose-p:mt-0 prose-blockquote:font-normal prose-blockquote:not-italic prose-blockquote:text-gray-700 prose-pre:mb-6! prose-pre:w-full prose-ul:my-2 prose-li:m-0 prose-li:mb-0.5 prose-li:[&>p]:mb-0 sm:prose-p:mb-4 mx-auto flex w-full max-w-[600px] grow flex-col items-start justify-center px-4 py-0 text-left text-sm sm:px-5 sm:text-lg">
|
||||
{guideRenderer.render(question.answer)}
|
||||
</div>
|
||||
|
||||
{question.isLongAnswer && (
|
||||
<div
|
||||
className={`qa-answer prose prose-h5:font-semibold prose-h5:mb-2 prose-h5:text-black prose-sm prose-quoteless mx-auto flex w-full max-w-[600px] grow flex-col items-start justify-center py-0 px-4 text-left text-sm prose-h1:mb-2.5 prose-h1:mt-7 prose-h2:mb-3 prose-h2:mt-0 prose-h3:mb-[5px] prose-h3:mt-[10px] prose-p:mb-2 prose-p:mt-0 prose-blockquote:font-normal prose-blockquote:not-italic prose-blockquote:text-gray-700 prose-pre:mb-6! prose-pre:w-full prose-ul:my-2 prose-li:m-0 prose-li:mb-0.5 sm:px-5 sm:text-lg sm:prose-p:mb-4`}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: markdownToHtml(question.answer, false),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="mt-7 text-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
|
||||
@@ -81,7 +81,7 @@ const showTableOfContent = tableOfContent.length > 0;
|
||||
---
|
||||
|
||||
<article class='lg:grid lg:max-w-full lg:grid-cols-[1fr_minmax(0,700px)_1fr]'>
|
||||
{
|
||||
<!-- {
|
||||
showTableOfContent && (
|
||||
<div class='bg-linear-to-r from-gray-50 py-0 lg:col-start-3 lg:col-end-4 lg:row-start-1'>
|
||||
<RelatedGuides
|
||||
@@ -92,7 +92,7 @@ const showTableOfContent = tableOfContent.length > 0;
|
||||
<TableOfContent toc={tableOfContent} client:load />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
} -->
|
||||
|
||||
<div
|
||||
class:list={[
|
||||
@@ -138,13 +138,13 @@ const showTableOfContent = tableOfContent.length > 0;
|
||||
You can either use these flashcards or jump to the questions list
|
||||
section below to see them in a list format.
|
||||
</p>
|
||||
<div class='mx-0 sm:-mb-32'>
|
||||
<!-- <div class='mx-0 sm:-mb-32'>
|
||||
<QuestionsList
|
||||
groupId={questionGroup.id}
|
||||
questions={questionGroup.questions}
|
||||
client:load
|
||||
/>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<h2 id='questions-list'>Questions List</h2>
|
||||
<p>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import type { QuestionType } from '../../lib/guide-renderer';
|
||||
import { QuestionsProgress } from './QuestionsProgress';
|
||||
import { CheckCircle, SkipForward, Sparkles } from 'lucide-react';
|
||||
import { QuestionCard } from './QuestionCard';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import type { QuestionType } from '../../lib/question-group';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { QuestionFinished } from './QuestionFinished';
|
||||
import { QuestionCard } from './QuestionCard';
|
||||
import { CheckCircleIcon, SkipForwardIcon, SparklesIcon } from 'lucide-react';
|
||||
import { Confetti } from '../Confetti';
|
||||
|
||||
type UserQuestionProgress = {
|
||||
@@ -16,12 +16,12 @@ type UserQuestionProgress = {
|
||||
export type QuestionProgressType = keyof UserQuestionProgress;
|
||||
|
||||
type QuestionsListProps = {
|
||||
groupId: string;
|
||||
questions: QuestionType[];
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function QuestionsList(props: QuestionsListProps) {
|
||||
const { questions } = props;
|
||||
const { questions, className } = props;
|
||||
|
||||
const [showConfetti, setShowConfetti] = useState(false);
|
||||
const [currQuestionIndex, setCurrQuestionIndex] = useState(0);
|
||||
@@ -73,7 +73,7 @@ export function QuestionsList(props: QuestionsListProps) {
|
||||
const hasFinished = hasProgress && currQuestionIndex === -1;
|
||||
|
||||
return (
|
||||
<div className="mb-0 gap-3 text-center sm:mb-40">
|
||||
<div className={cn('mb-0 gap-3 text-center sm:mb-40', className)}>
|
||||
<QuestionsProgress
|
||||
knowCount={knowCount}
|
||||
didNotKnowCount={dontKnowCount}
|
||||
@@ -139,9 +139,10 @@ export function QuestionsList(props: QuestionsListProps) {
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`flex flex-col gap-1 transition-opacity duration-300 sm:flex-row sm:gap-3 ${
|
||||
hasFinished ? 'opacity-0' : 'opacity-100'
|
||||
}`}
|
||||
className={cn(
|
||||
'flex flex-col gap-1 transition-opacity duration-300 sm:flex-row sm:gap-3',
|
||||
hasFinished ? 'opacity-0' : 'opacity-100',
|
||||
)}
|
||||
>
|
||||
<button
|
||||
disabled={!currQuestion}
|
||||
@@ -152,7 +153,7 @@ export function QuestionsList(props: QuestionsListProps) {
|
||||
}}
|
||||
className="flex flex-1 items-center rounded-md border border-gray-300 bg-white px-2 py-2 text-sm text-black transition-colors hover:border-black hover:bg-black hover:text-white disabled:pointer-events-none disabled:opacity-50 sm:rounded-lg sm:px-4 sm:py-3 sm:text-base"
|
||||
>
|
||||
<CheckCircle className="mr-1 h-4 text-current" />
|
||||
<CheckCircleIcon className="mr-1 h-4 text-current" />
|
||||
Already Know that
|
||||
</button>
|
||||
<button
|
||||
@@ -162,7 +163,7 @@ export function QuestionsList(props: QuestionsListProps) {
|
||||
disabled={!currQuestion}
|
||||
className="flex flex-1 items-center rounded-md border border-gray-300 bg-white px-2 py-2 text-sm text-black transition-colors hover:border-black hover:bg-black hover:text-white disabled:pointer-events-none disabled:opacity-50 sm:rounded-lg sm:px-4 sm:py-3 sm:text-base"
|
||||
>
|
||||
<Sparkles className="mr-1 h-4 text-current" />
|
||||
<SparklesIcon className="mr-1 h-4 text-current" />
|
||||
Didn't Know that
|
||||
</button>
|
||||
<button
|
||||
@@ -173,7 +174,7 @@ export function QuestionsList(props: QuestionsListProps) {
|
||||
data-next-question="skip"
|
||||
className="flex flex-1 items-center rounded-md border border-red-600 px-2 py-2 text-sm text-red-600 hover:bg-red-600 hover:text-white disabled:pointer-events-none disabled:opacity-50 sm:rounded-lg sm:px-4 sm:py-3 sm:text-base"
|
||||
>
|
||||
<SkipForward className="mr-1 h-4" />
|
||||
<SkipForwardIcon className="mr-1 h-4" />
|
||||
Skip Question
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import type { HeadingGroupType } from '../../lib/guide';
|
||||
import type { HeadingGroupType } from '../../lib/guide-renderer';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
@@ -23,7 +23,7 @@ export function TableOfContent(props: TableOfContentProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative min-w-[250px] px-5 pt-0 max-lg:min-w-full max-lg:max-w-full max-lg:border-none max-lg:px-0 lg:pt-5',
|
||||
'relative min-w-[250px] px-5 pt-0 max-lg:max-w-full max-lg:min-w-full max-lg:border-none max-lg:px-0 lg:pt-5',
|
||||
{
|
||||
'top-0 lg:sticky!': totalRows <= 20,
|
||||
},
|
||||
@@ -68,7 +68,7 @@ export function TableOfContent(props: TableOfContentProps) {
|
||||
</a>
|
||||
|
||||
{heading.children.length > 0 && (
|
||||
<ol className="my-0 ml-4 mt-1 space-y-0 max-lg:ml-0 max-lg:mt-0 max-lg:list-none">
|
||||
<ol className="my-0 mt-1 ml-4 space-y-0 max-lg:mt-0 max-lg:ml-0 max-lg:list-none">
|
||||
{heading.children.map((children) => {
|
||||
return (
|
||||
<li key={children.slug}>
|
||||
|
||||
@@ -0,0 +1,515 @@
|
||||
import { Fragment } from 'react';
|
||||
import type { JSX } from 'react/jsx-runtime';
|
||||
import type { JSONContent } from '@tiptap/core';
|
||||
import { slugify } from './slugger';
|
||||
import {
|
||||
CodeBlockContent,
|
||||
CodeBlockHeader,
|
||||
CodeBlockItem,
|
||||
} from '../components/Global/CodeBlock';
|
||||
import { QuestionsList } from '../components/Questions/QuestionsList';
|
||||
|
||||
export type HeadingType = {
|
||||
level: number;
|
||||
text: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type HeadingGroupType = HeadingType & { children: HeadingType[] };
|
||||
|
||||
export type QuestionType = {
|
||||
id: string;
|
||||
question: string;
|
||||
answer: JSONContent;
|
||||
topics: string[];
|
||||
};
|
||||
|
||||
export interface MarkType {
|
||||
[key: string]: any;
|
||||
type: string;
|
||||
attrs?: Record<string, any> | undefined;
|
||||
}
|
||||
|
||||
export class GuideRenderer {
|
||||
private marksOrder = ['underline', 'bold', 'italic', 'textStyle', 'link'];
|
||||
|
||||
render(content: JSONContent): JSX.Element[] {
|
||||
const nodes = content.content || [];
|
||||
const jsxNodes = nodes
|
||||
.map((node, index) => {
|
||||
const component = this.renderNode(node);
|
||||
if (!component) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Fragment key={`${node.type}-${index}`}>{component}</Fragment>;
|
||||
})
|
||||
.filter(Boolean) as JSX.Element[];
|
||||
|
||||
return jsxNodes;
|
||||
}
|
||||
|
||||
tableOfContents(node: JSONContent) {
|
||||
const headlines = this.headlines(node);
|
||||
let toc: HeadingGroupType[] = [];
|
||||
let currentGroup: HeadingGroupType | null = null;
|
||||
|
||||
const hasQASection = node.content?.some(
|
||||
(node) => node.type === 'qaSection',
|
||||
);
|
||||
|
||||
headlines
|
||||
.filter((heading) => heading.level !== 1)
|
||||
.forEach((heading) => {
|
||||
if (heading.level === 2) {
|
||||
currentGroup = { ...heading, children: [] };
|
||||
toc.push(currentGroup);
|
||||
} else if (currentGroup && heading.level === 3) {
|
||||
currentGroup.children.push({ ...heading, text: heading.text });
|
||||
}
|
||||
});
|
||||
|
||||
if (toc.length > 5) {
|
||||
toc.forEach((group) => {
|
||||
group.children = [];
|
||||
});
|
||||
}
|
||||
|
||||
const qaSection = node.content?.find((node) => node.type === 'qaSection');
|
||||
if (hasQASection && qaSection) {
|
||||
toc.push({
|
||||
level: 2,
|
||||
text: 'Test yourself with Flashcards',
|
||||
slug: 'test-with-flashcards',
|
||||
children: [],
|
||||
});
|
||||
|
||||
const questions = this.questions(qaSection);
|
||||
const topicsInOrder = [
|
||||
...new Set(
|
||||
questions
|
||||
.map((question) => question.topics)
|
||||
.flat()
|
||||
.filter(Boolean),
|
||||
),
|
||||
];
|
||||
|
||||
toc.push({
|
||||
level: 2,
|
||||
text: 'Questions List',
|
||||
slug: 'questions-list',
|
||||
children: topicsInOrder.map((topic) => {
|
||||
let topicText = topic;
|
||||
let topicSlug = slugify(topic);
|
||||
if (topic.toLowerCase() === 'beginners') {
|
||||
topicText = 'Beginner Level';
|
||||
topicSlug = 'beginner-level';
|
||||
} else if (topic.toLowerCase() === 'intermediate') {
|
||||
topicText = 'Intermediate Level';
|
||||
topicSlug = 'intermediate-level';
|
||||
} else if (topic.toLowerCase() === 'advanced') {
|
||||
topicText = 'Advanced Level';
|
||||
topicSlug = 'advanced-level';
|
||||
}
|
||||
|
||||
return {
|
||||
level: 2,
|
||||
children: [],
|
||||
slug: topicSlug,
|
||||
text: topicText,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return toc;
|
||||
}
|
||||
|
||||
headlines(node: JSONContent) {
|
||||
const nodes = node.content || [];
|
||||
const headlines: Array<HeadingType> = [];
|
||||
|
||||
let hasFoundQASection = false;
|
||||
const extractHeadlines = (node: JSONContent) => {
|
||||
if (node.type === 'qaSection' || hasFoundQASection) {
|
||||
hasFoundQASection = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === 'heading') {
|
||||
const text = this.getText(node);
|
||||
headlines.push({
|
||||
level: node.attrs?.level || 1,
|
||||
text,
|
||||
slug: slugify(text),
|
||||
});
|
||||
}
|
||||
|
||||
if (node.content) {
|
||||
node.content.forEach((childNode) => {
|
||||
extractHeadlines(childNode);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
nodes.forEach((childNode) => {
|
||||
extractHeadlines(childNode);
|
||||
});
|
||||
|
||||
return headlines;
|
||||
}
|
||||
|
||||
private getText(node: JSONContent): string {
|
||||
if (node.type === 'text') {
|
||||
return node.text || '';
|
||||
}
|
||||
|
||||
if (node.content) {
|
||||
return node.content.map((childNode) => this.getText(childNode)).join('');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// `content` will call corresponding node type
|
||||
// and return text content
|
||||
private content(node: JSONContent): JSX.Element[] {
|
||||
const allNodes = node.content || [];
|
||||
return allNodes
|
||||
.map((childNode, index) => {
|
||||
const component = this.renderNode(childNode);
|
||||
if (!component) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment key={`${childNode.type}-${index}`}>{component}</Fragment>
|
||||
);
|
||||
})
|
||||
.filter(Boolean) as JSX.Element[];
|
||||
}
|
||||
|
||||
// `renderNode` will call the method of the corresponding node type
|
||||
private renderNode(node: JSONContent): JSX.Element | null {
|
||||
const type = node.type || '';
|
||||
|
||||
if (type in this) {
|
||||
// @ts-expect-error - `this` is not assignable to type 'never'
|
||||
return this[type]?.(node) as JSX.Element;
|
||||
}
|
||||
|
||||
console.log(`Node type "${type}" is not supported.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// `renderMark` will call the method of the corresponding mark type
|
||||
private renderMark(node: JSONContent): JSX.Element {
|
||||
// It will wrap the text with the corresponding mark type
|
||||
const text = node?.text || <> </>;
|
||||
let marks = node?.marks || [];
|
||||
// sort the marks by uderline, bold, italic, textStyle, link
|
||||
// so that the text will be wrapped in the correct order
|
||||
marks.sort((a, b) => {
|
||||
return this.marksOrder.indexOf(a.type) - this.marksOrder.indexOf(b.type);
|
||||
});
|
||||
|
||||
return marks.reduce(
|
||||
(acc, mark) => {
|
||||
const type = mark.type;
|
||||
if (type in this) {
|
||||
// @ts-expect-error - `this` is not assignable to type 'never'
|
||||
return this[type]?.(mark, acc) as JSX.Element;
|
||||
}
|
||||
|
||||
throw new Error(`Mark type "${type}" is not supported.`);
|
||||
},
|
||||
<>{text}</>,
|
||||
);
|
||||
}
|
||||
|
||||
private paragraph(node: JSONContent): JSX.Element {
|
||||
const isEmpty =
|
||||
!node.content ||
|
||||
node.content?.every(
|
||||
(child) => child.type === 'text' && child.text === '',
|
||||
);
|
||||
if (isEmpty) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return <p>{this.content(node)}</p>;
|
||||
}
|
||||
|
||||
private text(node: JSONContent): JSX.Element {
|
||||
if (node.marks) {
|
||||
return this.renderMark(node);
|
||||
}
|
||||
|
||||
const text = node.text;
|
||||
return text ? <>{text}</> : <> </>;
|
||||
}
|
||||
|
||||
private bold(_: MarkType, text: JSX.Element): JSX.Element {
|
||||
return <strong>{text}</strong>;
|
||||
}
|
||||
|
||||
private italic(_: MarkType, text: JSX.Element): JSX.Element {
|
||||
return <em>{text}</em>;
|
||||
}
|
||||
|
||||
private underline(_: MarkType, text: JSX.Element): JSX.Element {
|
||||
return <u>{text}</u>;
|
||||
}
|
||||
|
||||
private strike(_: MarkType, text: JSX.Element): JSX.Element {
|
||||
return <s style={{ textDecoration: 'line-through' }}>{text}</s>;
|
||||
}
|
||||
|
||||
private textStyle(mark: MarkType, text: JSX.Element): JSX.Element {
|
||||
const { attrs } = mark;
|
||||
const { color = 'inherit' } = attrs || {};
|
||||
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
color,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
private link(mark: MarkType, text: JSX.Element): JSX.Element {
|
||||
const { attrs } = mark;
|
||||
const { href } = attrs || {};
|
||||
|
||||
const isExternal = href?.startsWith('http');
|
||||
const isRoadmapUrl = href?.startsWith('https://roadmap.sh/');
|
||||
|
||||
const rel = isExternal && !isRoadmapUrl ? 'noopener noreferrer' : undefined;
|
||||
|
||||
return (
|
||||
<a href={href} target="_blank" rel={rel}>
|
||||
{text}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
private heading(node: JSONContent): JSX.Element {
|
||||
const { attrs } = node;
|
||||
const { level } = attrs || {};
|
||||
|
||||
const text = this.getText(node);
|
||||
const slug = slugify(text);
|
||||
|
||||
let Comp: keyof JSX.IntrinsicElements = 'h1';
|
||||
if (level === 2) {
|
||||
Comp = 'h2';
|
||||
} else if (level === 3) {
|
||||
Comp = 'h3';
|
||||
} else if (level === 4) {
|
||||
Comp = 'h4';
|
||||
} else if (level === 5) {
|
||||
Comp = 'h5';
|
||||
} else if (level === 6) {
|
||||
Comp = 'h6';
|
||||
}
|
||||
|
||||
return <Comp id={slug}>{this.content(node)}</Comp>;
|
||||
}
|
||||
|
||||
private horizontalRule(_: JSONContent): JSX.Element {
|
||||
return <hr />;
|
||||
}
|
||||
|
||||
private orderedList(node: JSONContent): JSX.Element {
|
||||
return <ol>{this.content(node)}</ol>;
|
||||
}
|
||||
|
||||
private bulletList(node: JSONContent): JSX.Element {
|
||||
return <ul>{this.content(node)}</ul>;
|
||||
}
|
||||
|
||||
private listItem(node: JSONContent): JSX.Element {
|
||||
return <li>{this.content(node)}</li>;
|
||||
}
|
||||
|
||||
private hardBreak(_: JSONContent): JSX.Element {
|
||||
return <br />;
|
||||
}
|
||||
|
||||
private image(node: JSONContent): JSX.Element {
|
||||
const { attrs } = node;
|
||||
const { src, alt } = attrs || {};
|
||||
|
||||
return <img alt={alt || 'Image'} src={src} />;
|
||||
}
|
||||
|
||||
private code(_: MarkType, text: JSX.Element): JSX.Element {
|
||||
return <code>{text}</code>;
|
||||
}
|
||||
|
||||
private codeBlock(node: JSONContent): JSX.Element {
|
||||
const code = this.getText(node);
|
||||
const language = node.attrs?.language || 'javascript';
|
||||
|
||||
return (
|
||||
<div className="not-prose my-6 w-full max-w-full overflow-hidden rounded-lg border border-gray-200">
|
||||
<CodeBlockHeader language={language} code={code} />
|
||||
|
||||
<CodeBlockItem key={language} value={language} lineNumbers={false}>
|
||||
<CodeBlockContent language={language}>{code}</CodeBlockContent>
|
||||
</CodeBlockItem>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private blockquote(node: JSONContent): JSX.Element {
|
||||
return <blockquote>{this.content(node)}</blockquote>;
|
||||
}
|
||||
|
||||
questions(node: JSONContent) {
|
||||
const content = node.content || [];
|
||||
const questions: QuestionType[] = [];
|
||||
let currentTopic: string | null = null;
|
||||
let currentQuestion: QuestionType | null = null;
|
||||
|
||||
for (const childNode of content) {
|
||||
switch (childNode.type) {
|
||||
case 'heading':
|
||||
// if level is 2, it's a topic
|
||||
if (childNode.attrs?.level === 2) {
|
||||
currentTopic = this.getText(childNode);
|
||||
// if level is 3, it's a question
|
||||
} else if (childNode.attrs?.level === 3) {
|
||||
if (currentTopic) {
|
||||
const questionText = this.getText(childNode);
|
||||
currentQuestion = {
|
||||
id: slugify(questionText),
|
||||
question: questionText,
|
||||
answer: {
|
||||
type: 'doc',
|
||||
content: [],
|
||||
},
|
||||
topics: [currentTopic],
|
||||
};
|
||||
questions.push(currentQuestion);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// anything else is an answer
|
||||
default:
|
||||
if (!currentQuestion || !currentQuestion.answer.content) {
|
||||
console.warn('No current question found');
|
||||
continue;
|
||||
}
|
||||
|
||||
currentQuestion.answer.content.push(childNode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return questions;
|
||||
}
|
||||
|
||||
private qaSection(node: JSONContent): JSX.Element {
|
||||
const questions = this.questions(node);
|
||||
|
||||
const questionsGroupedByTopics = questions.reduce(
|
||||
(acc, question) => {
|
||||
question.topics?.forEach((topic) => {
|
||||
acc[topic] = [...(acc[topic] || []), question];
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, QuestionType[]>,
|
||||
);
|
||||
|
||||
const topicsInOrder = [
|
||||
...new Set(
|
||||
questions
|
||||
.map((question) => question.topics)
|
||||
.flat()
|
||||
.filter(Boolean),
|
||||
),
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 id="test-with-flashcards">Test yourself with Flashcards</h2>
|
||||
<p>
|
||||
You can either use these flashcards or jump to the questions list
|
||||
section below to see them in a list format.
|
||||
</p>
|
||||
|
||||
<div className="mx-0 sm:-mb-32">
|
||||
<QuestionsList questions={questions} />
|
||||
</div>
|
||||
|
||||
<h2 id="questions-list">Questions List</h2>
|
||||
<p>
|
||||
If you prefer to see the questions in a list format, you can find them
|
||||
below.
|
||||
</p>
|
||||
|
||||
{topicsInOrder.map((questionLevel) => (
|
||||
<div className="mb-5" key={questionLevel}>
|
||||
<h3 id={slugify(questionLevel)} className="mb-0 capitalize">
|
||||
{questionLevel.toLowerCase() === 'beginners'
|
||||
? 'Beginner Level'
|
||||
: questionLevel.toLowerCase() === 'intermediate'
|
||||
? 'Intermediate Level'
|
||||
: questionLevel.toLowerCase() === 'advanced'
|
||||
? 'Advanced Level'
|
||||
: questionLevel}
|
||||
</h3>
|
||||
{questionsGroupedByTopics[questionLevel].map((q) => (
|
||||
<div className="mb-5" key={q.id}>
|
||||
<h4>{q.question}</h4>
|
||||
<div>{this.render(q.answer)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private table(node: JSONContent): JSX.Element {
|
||||
const content = node.content || [];
|
||||
const rows = content.filter((node) => node.type === 'tableRow');
|
||||
const firstRow = rows?.[0];
|
||||
const hasTableHead = firstRow?.content?.some(
|
||||
(node) => node.type === 'tableHeader',
|
||||
);
|
||||
|
||||
const remainingRows = rows.slice(hasTableHead ? 1 : 0);
|
||||
|
||||
return (
|
||||
<table className="[&_p]:m-0">
|
||||
{hasTableHead && <thead>{this.renderNode(firstRow)}</thead>}
|
||||
<tbody>
|
||||
{this.render({
|
||||
type: 'doc',
|
||||
content: remainingRows,
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
private tableRow(node: JSONContent): JSX.Element {
|
||||
return <tr>{this.content(node)}</tr>;
|
||||
}
|
||||
|
||||
private tableHeader(node: JSONContent): JSX.Element {
|
||||
return <th>{this.content(node)}</th>;
|
||||
}
|
||||
|
||||
private tableCell(node: JSONContent): JSX.Element {
|
||||
return <td>{this.content(node)}</td>;
|
||||
}
|
||||
}
|
||||
|
||||
export const guideRenderer = new GuideRenderer();
|
||||
+23
-2
@@ -3,8 +3,22 @@ type RoadmapOpenGraphQuery = {
|
||||
resourceId: string;
|
||||
};
|
||||
|
||||
export function getOpenGraphImageUrl(params: RoadmapOpenGraphQuery) {
|
||||
return `${import.meta.env.DEV ? 'http://localhost:3000' : 'https://roadmap.sh'}/og/${params.group}/${params.resourceId}`;
|
||||
export function getOpenGraphImageUrl(
|
||||
params: RoadmapOpenGraphQuery,
|
||||
query?: Record<string, string>,
|
||||
) {
|
||||
const baseUrl = import.meta.env.DEV
|
||||
? 'http://localhost:3000'
|
||||
: 'https://roadmap.sh';
|
||||
|
||||
const url = new URL(`${baseUrl}/og/${params.group}/${params.resourceId}`);
|
||||
if (query) {
|
||||
Object.entries(query).forEach(([key, value]) => {
|
||||
url.searchParams.set(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
return String(url);
|
||||
}
|
||||
|
||||
export async function getDefaultOpenGraphImageBuffer() {
|
||||
@@ -15,11 +29,18 @@ export async function getDefaultOpenGraphImageBuffer() {
|
||||
export async function getResourceOpenGraph(
|
||||
type: 'roadmap' | 'guide' | 'best-practice',
|
||||
resourceId: string,
|
||||
query?: Record<string, string>,
|
||||
) {
|
||||
const url = new URL(`${import.meta.env.PUBLIC_API_URL}/v1-open-graph`);
|
||||
url.searchParams.set('type', type);
|
||||
url.searchParams.set('resourceId', resourceId);
|
||||
url.searchParams.set('variant', 'image');
|
||||
if (query) {
|
||||
Object.entries(query).forEach(([key, value]) => {
|
||||
url.searchParams.set(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
|
||||
return response.text();
|
||||
|
||||
@@ -43,15 +43,19 @@ export async function httpCall<ResponseType = AppResponse>(
|
||||
? url
|
||||
: `${import.meta.env.PUBLIC_API_URL}${url}`;
|
||||
try {
|
||||
const fingerprintPromise = await fp.load();
|
||||
const fingerprint = await fingerprintPromise.get();
|
||||
let visitorId = '';
|
||||
if (typeof window !== 'undefined') {
|
||||
const fingerprintPromise = await fp.load();
|
||||
const fingerprint = await fingerprintPromise.get();
|
||||
visitorId = fingerprint.visitorId;
|
||||
}
|
||||
|
||||
const isMultiPartFormData = options?.body instanceof FormData;
|
||||
|
||||
const headers = new Headers({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${Cookies.get(TOKEN_COOKIE_NAME)}`,
|
||||
fp: fingerprint.visitorId,
|
||||
...(visitorId ? { fp: visitorId } : {}),
|
||||
...(options?.headers ?? {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,13 @@ import path from 'node:path';
|
||||
import matter from 'gray-matter';
|
||||
import MarkdownIt from 'markdown-it-async';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
getOfficialGuide,
|
||||
type OfficialGuideResponse,
|
||||
} from '../../queries/official-guide';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { GuideContent } from '../../components/Guide/GuideContent';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
@@ -16,72 +23,108 @@ if (!topicId || !roadmapId) {
|
||||
return Astro.rewrite('/404');
|
||||
}
|
||||
|
||||
// Handle nested paths by joining the segments
|
||||
const topicPath = Array.isArray(topicId) ? topicId.join('/') : topicId;
|
||||
const isTopic = topicId?.includes('@') || topicId?.includes('/');
|
||||
let gitHubUrl = '';
|
||||
let htmlContent = '';
|
||||
let guide: OfficialGuideResponse | null = null;
|
||||
let permalink = '';
|
||||
let ogImageUrl = '';
|
||||
|
||||
// Get the project root directory
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
if (isTopic) {
|
||||
// Handle nested paths by joining the segments
|
||||
const topicPath = Array.isArray(topicId) ? topicId.join('/') : topicId;
|
||||
|
||||
// hack to make it work. TODO: Fix
|
||||
const projectRoot = path.resolve(__dirname, '../../..').replace(/dist$/, '');
|
||||
// Get the project root directory
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Construct the path to the markdown file
|
||||
let contentPath = path.join(
|
||||
projectRoot,
|
||||
'src',
|
||||
'data',
|
||||
'roadmaps',
|
||||
roadmapId,
|
||||
'content',
|
||||
`${topicPath}.md`,
|
||||
);
|
||||
// hack to make it work. TODO: Fix
|
||||
const projectRoot = path.resolve(__dirname, '../../..').replace(/dist$/, '');
|
||||
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(contentPath)) {
|
||||
const indexFilePath = path.join(
|
||||
// Construct the path to the markdown file
|
||||
let contentPath = path.join(
|
||||
projectRoot,
|
||||
'src',
|
||||
'data',
|
||||
'roadmaps',
|
||||
roadmapId,
|
||||
'content',
|
||||
`${topicPath}/index.md`,
|
||||
`${topicPath}.md`,
|
||||
);
|
||||
|
||||
if (!fs.existsSync(indexFilePath)) {
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(contentPath)) {
|
||||
const indexFilePath = path.join(
|
||||
projectRoot,
|
||||
'src',
|
||||
'data',
|
||||
'roadmaps',
|
||||
roadmapId,
|
||||
'content',
|
||||
`${topicPath}/index.md`,
|
||||
);
|
||||
|
||||
if (!fs.existsSync(indexFilePath)) {
|
||||
Astro.response.status = 404;
|
||||
Astro.response.statusText = 'Not found';
|
||||
|
||||
return Astro.rewrite('/404');
|
||||
}
|
||||
|
||||
contentPath = indexFilePath;
|
||||
}
|
||||
|
||||
// Read and parse the markdown file
|
||||
const fileContent = fs.readFileSync(contentPath, 'utf-8');
|
||||
const { content } = matter(fileContent);
|
||||
|
||||
const fileWithoutBasePath = contentPath.replace(
|
||||
/.+?\/src\/data/,
|
||||
'/src/data',
|
||||
);
|
||||
|
||||
const md = MarkdownIt();
|
||||
|
||||
gitHubUrl = `https://github.com/kamranahmedse/developer-roadmap/tree/master${fileWithoutBasePath}`;
|
||||
htmlContent = await md.renderAsync(content);
|
||||
} else {
|
||||
guide = await getOfficialGuide(topicId, roadmapId);
|
||||
if (!guide) {
|
||||
Astro.response.status = 404;
|
||||
Astro.response.statusText = 'Not found';
|
||||
|
||||
return Astro.rewrite('/404');
|
||||
}
|
||||
|
||||
contentPath = indexFilePath;
|
||||
permalink = `/${roadmapId}/${topicId}`;
|
||||
ogImageUrl =
|
||||
guide?.featuredImage ||
|
||||
getOpenGraphImageUrl(
|
||||
{
|
||||
group: 'guide',
|
||||
resourceId: topicId,
|
||||
},
|
||||
{
|
||||
roadmapId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Read and parse the markdown file
|
||||
const fileContent = fs.readFileSync(contentPath, 'utf-8');
|
||||
const { data: frontmatter, content } = matter(fileContent);
|
||||
|
||||
// Get the roadmap metadata
|
||||
const roadmapPath = path.join(
|
||||
projectRoot,
|
||||
'src',
|
||||
'data',
|
||||
'roadmaps',
|
||||
roadmapId,
|
||||
`${roadmapId}.md`,
|
||||
);
|
||||
const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
const { data: roadmapData } = matter(roadmapContent);
|
||||
|
||||
const fileWithoutBasePath = contentPath.replace(/.+?\/src\/data/, '/src/data');
|
||||
const gitHubUrl = `https://github.com/kamranahmedse/developer-roadmap/tree/master${fileWithoutBasePath}`;
|
||||
|
||||
const md = MarkdownIt();
|
||||
const htmlContent = await md.renderAsync(content);
|
||||
---
|
||||
|
||||
<div data-github-url={gitHubUrl}></div>
|
||||
|
||||
<Fragment set:html={htmlContent} />
|
||||
{
|
||||
isTopic ? (
|
||||
<>
|
||||
<div data-github-url={gitHubUrl} />
|
||||
<Fragment set:html={htmlContent} />
|
||||
</>
|
||||
) : (
|
||||
<BaseLayout
|
||||
title={guide?.seo?.metaTitle ?? guide?.title ?? ''}
|
||||
description={guide?.seo?.metaDescription ?? guide?.description ?? ''}
|
||||
permalink={permalink}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} client:load />
|
||||
<div slot='changelog-banner' />
|
||||
</BaseLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'ai-data-scientist-career-path';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/ai-data-scientist/career-path`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'ai-data-scientist-lifecycle';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/ai-data-scientist/lifecycle`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'ai-data-scientist-skills';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/ai-data-scientist/skills`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'ai-data-scientist-tools';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/ai-data-scientist/tools`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'ai-data-scientist-vs-ai';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/ai-data-scientist/vs-ai`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'ai-data-scientist-vs-business-analytics';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/ai-data-scientist/vs-business-analytics`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'ai-data-scientist-vs-computer-science';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/ai-data-scientist/vs-computer-science`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'ai-data-scientist-vs-cyber-security';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/ai-data-scientist/vs-cyber-security`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'ai-data-scientist-vs-data-analytics';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/ai-data-scientist/vs-data-analytics`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'ai-data-scientist-vs-data-engineering';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/ai-data-scientist/vs-data-engineering`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'ai-data-scientist-vs-machine-learning';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/ai-data-scientist/vs-machine-learning`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'ai-data-scientist-vs-software-engineering';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/ai-data-scientist/vs-software-engineering`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'ai-data-scientist-vs-statistics';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/ai-data-scientist/vs-statistics`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,153 +0,0 @@
|
||||
---
|
||||
import AstroIcon from '../../components/AstroIcon.astro';
|
||||
import { GuideListItem } from '../../components/FeaturedGuides/GuideListItem';
|
||||
import { VideoListItem } from '../../components/FeaturedVideos/VideoListItem';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getAuthorById, getAuthorIds } from '../../lib/author';
|
||||
import { getGuidesByAuthor } from '../../lib/guide';
|
||||
import { getAllQuestionGroups } from '../../lib/question-group';
|
||||
import { getVideosByAuthor } from '../../lib/video';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
interface Params extends Record<string, string | undefined> {}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const authorIds = await getAuthorIds();
|
||||
|
||||
return authorIds.map((authorId) => ({
|
||||
params: { authorId },
|
||||
}));
|
||||
}
|
||||
|
||||
const { authorId } = Astro.params;
|
||||
|
||||
const author = await getAuthorById(authorId);
|
||||
const authorFrontmatter = author.frontmatter;
|
||||
|
||||
const guides = await getGuidesByAuthor(authorId);
|
||||
const questionGuides = (await getAllQuestionGroups()).filter(
|
||||
(group) => group.frontmatter.authorId === authorId,
|
||||
);
|
||||
const videos = await getVideosByAuthor(authorId);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
permalink={`/authors/${author.id}`}
|
||||
title={`${author.frontmatter.name} - Author at roadmap.sh`}
|
||||
briefTitle={author.frontmatter.name}
|
||||
ogImageUrl={`https://roadmap.sh/${authorFrontmatter.imageUrl}`}
|
||||
description={`${author.frontmatter.name} has written ${guides.length} articles on roadmap.sh on a variety of topics.`}
|
||||
noIndex={false}
|
||||
jsonLd={[
|
||||
{
|
||||
'@context': 'https://schema.org/',
|
||||
'@type': 'Person',
|
||||
name: authorFrontmatter.name,
|
||||
url: `https://roadmap.sh/authors/${authorId}`,
|
||||
image: authorFrontmatter.imageUrl.startsWith('http')
|
||||
? authorFrontmatter.imageUrl
|
||||
: `https://roadmap.sh${authorFrontmatter.imageUrl}`,
|
||||
sameAs: authorFrontmatter.social
|
||||
? Object.values(authorFrontmatter.social)
|
||||
: [],
|
||||
...(authorFrontmatter.employment && {
|
||||
jobTitle: authorFrontmatter.employment?.title,
|
||||
worksFor: {
|
||||
'@type': 'Organization',
|
||||
name: authorFrontmatter.employment.company,
|
||||
},
|
||||
}),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div class='container pb-0 pt-4 md:pb-16 md:pt-8'>
|
||||
<div class=''>
|
||||
<div class='mb-5 flex items-center gap-8 rounded-3xl py-0 md:py-8'>
|
||||
<div class='grow'>
|
||||
<h1 class='text-2xl font-bold md:text-3xl'>
|
||||
{authorFrontmatter.name}
|
||||
</h1>
|
||||
<div
|
||||
class='mb-4 mt-1 flex flex-col gap-3 leading-normal text-gray-800 md:mb-6 md:mt-4 [&>p>a]:font-semibold [&>p>a]:underline'
|
||||
>
|
||||
<author.Content />
|
||||
</div>
|
||||
|
||||
<div class='flex items-center justify-between'>
|
||||
<div class='flex items-center gap-1.5'>
|
||||
{
|
||||
authorFrontmatter.social?.github && (
|
||||
<a
|
||||
href={authorFrontmatter.social.github}
|
||||
target='_blank'
|
||||
class='text-gray-500 transition-colors hover:text-gray-800'
|
||||
>
|
||||
<AstroIcon icon='github' class='h-[20px]' />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
{
|
||||
authorFrontmatter.social.twitter && (
|
||||
<a
|
||||
href={authorFrontmatter.social.twitter}
|
||||
target='_blank'
|
||||
class='text-gray-500 transition-colors hover:text-gray-800'
|
||||
>
|
||||
<AstroIcon icon='twitter' class='h-[20px]' />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
{
|
||||
authorFrontmatter.social.linkedin && (
|
||||
<a
|
||||
href={authorFrontmatter.social.linkedin}
|
||||
target='_blank'
|
||||
class='text-gray-500 transition-colors hover:text-gray-800'
|
||||
>
|
||||
<AstroIcon icon='linkedin-2' class='h-[20px]' />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
{
|
||||
authorFrontmatter.social.website && (
|
||||
<a
|
||||
href={authorFrontmatter.social.website}
|
||||
target='_blank'
|
||||
class='text-gray-500 transition-colors hover:text-gray-800'
|
||||
>
|
||||
<AstroIcon icon='globe' class='h-[20px]' />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='hidden shrink-0 flex-col md:flex'>
|
||||
<img
|
||||
alt="Kamran Ahmed's profile picture"
|
||||
class='block h-[175px] w-[175px] rounded-full bg-gray-100'
|
||||
src={authorFrontmatter.imageUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class='rounded-t-xl bg-linear-to-b from-gray-100 to-white px-3 py-2 md:px-6 md:py-3 [&>*:last-child]:border-b-0'
|
||||
>
|
||||
{
|
||||
[...guides, ...questionGuides]
|
||||
.sort((a, b) => {
|
||||
const aFrontmatter = a.frontmatter as any;
|
||||
const bFrontmatter = b.frontmatter as any;
|
||||
|
||||
const aDate = aFrontmatter.date || aFrontmatter.publishedAt;
|
||||
const bDate = bFrontmatter.date || bFrontmatter.publishedAt;
|
||||
return new Date(bDate).getTime() - new Date(aDate).getTime();
|
||||
})
|
||||
.map((guide) => <GuideListItem guide={guide} />)
|
||||
}
|
||||
{videos.map((video) => <VideoListItem video={video} />)}
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getAuthorById, getAuthorIds } from '../../lib/author';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const authorIds = await getAuthorIds();
|
||||
|
||||
return await Promise.all(
|
||||
authorIds.map(async (authorId) => {
|
||||
const authorDetails = await getAuthorById(authorId);
|
||||
|
||||
return {
|
||||
params: { authorId },
|
||||
props: {
|
||||
authorDetails: 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',
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'backend-developer-skills';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/backend/developer-skills`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'backend-developer-tools';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/backend/developer-tools`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'backend-frameworks';
|
||||
const guide = await getGuideById(guideId).catch(() => null);
|
||||
if (!guide) {
|
||||
return Astro.redirect('/404');
|
||||
}
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/backend/frameworks`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'backend-job-description';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo?.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={replaceVariables(guideData?.seo?.title)}
|
||||
description={replaceVariables(guideData?.seo?.description)}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
</BaseLayout>
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'backend-languages';
|
||||
const guide = await getGuideById(guideId).catch(() => null);
|
||||
if (!guide) {
|
||||
return Astro.redirect('/404');
|
||||
}
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/backend/languages`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'backend-project-ideas';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/backend/project-ideas`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'backend-technologies';
|
||||
const guide = await getGuideById(guideId).catch(() => null);
|
||||
if (!guide) {
|
||||
return Astro.redirect('/404');
|
||||
}
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'data-analyst-career-path';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/data-analyst/career-path`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'data-analyst-how-to-become';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/data-analyst/how-to-become`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'data-analyst-vs-data-engineer';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/data-analyst/vs-data-engineer`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'devops-automation-tools';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'devops-automation';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'devops-best-practices';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'devops-career-path';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={replaceVariables(guideData.seo.title)}
|
||||
description={replaceVariables(guideData.seo.description)}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const guideId = 'devops-engineer';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'devops-vs-sre';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'how-to-become-devops-engineer';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'devops-job-description';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'devops-lifecycle';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'devops-principles';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'devops-shift-left-testing';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'devops-skills';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'devops-test-automation';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'devops-tools';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'devops-vs-agile';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'devops-vs-developer';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={replaceVariables(guideData.seo.title)}
|
||||
description={replaceVariables(guideData.seo.description)}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'devops-vs-devsecops';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'devops-vs-full-stack';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'frontend-developer-skills';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'frontend-frameworks';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'how-to-become-frontend-developer';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'frontend-job-description';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo?.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={replaceVariables(guideData?.seo?.title)}
|
||||
description={replaceVariables(guideData?.seo?.description)}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'frontend-languages';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'frontend-technologies';
|
||||
const guide = await getGuideById(guideId).catch(() => null);
|
||||
if (!guide) {
|
||||
return Astro.redirect('/404');
|
||||
}
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'frontend-web-developer-portfolio';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'full-stack-developer-skills';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'full-stack-how-to-become';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'full-stack-job-description';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'full-stack-vs-backend';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'full-stack-vs-software-engineer';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
const guideId = 'golang-rest-api';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'go-vs-java';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getAllGuides, type GuideFileType } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export interface Props {
|
||||
guide: GuideFileType;
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const guides = (await getAllGuides()).filter(
|
||||
(guide) => !guide.frontmatter.excludedBySlug,
|
||||
);
|
||||
|
||||
return guides.map((guide) => ({
|
||||
params: { guideId: guide.id },
|
||||
props: { guide },
|
||||
}));
|
||||
}
|
||||
|
||||
const { guideId } = Astro.params;
|
||||
const { guide } = Astro.props;
|
||||
const { frontmatter: guideData, author } = guide;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/guides/${guideId}`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
import { GuideListItem } from '../../components/FeaturedGuides/GuideListItem';
|
||||
import SimplePageHeader from '../../components/SimplePageHeader.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getAllGuides } from '../../lib/guide';
|
||||
import { getAllQuestionGroups } from '../../lib/question-group';
|
||||
|
||||
const guides = await getAllGuides();
|
||||
const questionGuides = (await getAllQuestionGroups()).filter(
|
||||
(questionGroup) => questionGroup.frontmatter.authorId,
|
||||
);
|
||||
|
||||
const allGuides = [...guides, ...questionGuides];
|
||||
const sortedGuides = allGuides.sort((a, b) => {
|
||||
const aDate = new Date(a.frontmatter.date as string);
|
||||
const bDate = new Date(b.frontmatter.date as string);
|
||||
|
||||
return bDate.getTime() - aDate.getTime();
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title='Guides - roadmap.sh'
|
||||
description={'Detailed guides on Software Engineering Topics'}
|
||||
permalink={`/guides`}
|
||||
>
|
||||
<SimplePageHeader
|
||||
title='Guides'
|
||||
description='Succinct graphical explanations to engineering topics.'
|
||||
/>
|
||||
|
||||
<div class='bg-gray-50 pb-20 pt-2'>
|
||||
<div class='container'>
|
||||
<div class='mt-3 sm:my-5'>
|
||||
{sortedGuides.map((guide) => <GuideListItem guide={guide} />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
+4
-16
@@ -6,15 +6,13 @@ import { FeaturedVideoList } from '../components/FeaturedVideos/FeaturedVideoLis
|
||||
import HeroSection from '../components/HeroSection/HeroSection.astro';
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { getAllBestPractices } from '../lib/best-practice';
|
||||
import { getAllGuides } from '../lib/guide';
|
||||
import { getAllQuestionGroups } from '../lib/question-group';
|
||||
import { getRoadmapsByTag } from '../lib/roadmap';
|
||||
import { getAllVideos } from '../lib/video';
|
||||
import { listOfficialGuides } from '../queries/official-guide';
|
||||
|
||||
const roleRoadmaps = await getRoadmapsByTag('role-roadmap');
|
||||
const skillRoadmaps = await getRoadmapsByTag('skill-roadmap');
|
||||
const bestPractices = await getAllBestPractices();
|
||||
const questionGroups = await getAllQuestionGroups();
|
||||
|
||||
export const projectGroups = [
|
||||
{
|
||||
@@ -31,9 +29,9 @@ export const projectGroups = [
|
||||
},
|
||||
];
|
||||
|
||||
const guides = await getAllGuides();
|
||||
const questionGuides = (await getAllQuestionGroups()).filter(
|
||||
(questionGroup) => questionGroup.frontmatter.authorId,
|
||||
const guides = await listOfficialGuides();
|
||||
const questionGuides = guides.filter(
|
||||
(guide) => guide.roadmapId === 'questions' && !!guide?.authorId,
|
||||
);
|
||||
const videos = await getAllVideos();
|
||||
---
|
||||
@@ -97,16 +95,6 @@ const videos = await getAllVideos();
|
||||
}))}
|
||||
/>
|
||||
|
||||
<FeaturedItems
|
||||
heading='Questions'
|
||||
allowBookmark={false}
|
||||
featuredItems={questionGroups.map((questionGroup) => ({
|
||||
text: questionGroup.frontmatter.briefTitle,
|
||||
url: `/questions/${questionGroup.id}`,
|
||||
isNew: questionGroup.frontmatter.isNew,
|
||||
}))}
|
||||
/>
|
||||
|
||||
<div class='grid grid-cols-1 gap-7 bg-gray-50 py-7 sm:gap-16 sm:py-16'>
|
||||
<FeaturedGuideList
|
||||
heading='Guides'
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'java-developer-skills';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'java-vs-javascript';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'java-vs-python';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner" />
|
||||
</BaseLayout>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'javascript-vs-typescript';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/javascript/vs-typescript`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
getDefaultOpenGraphImageBuffer,
|
||||
getResourceOpenGraph,
|
||||
} from '../../../lib/open-graph';
|
||||
import {Transformer} from "@napi-rs/image";
|
||||
import { Transformer } from '@napi-rs/image';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
@@ -13,6 +13,7 @@ type Params = {
|
||||
|
||||
export const GET: APIRoute<any, Params> = async (context) => {
|
||||
const { slug } = context.params;
|
||||
const { searchParams } = context.url;
|
||||
|
||||
if (!slug) {
|
||||
const buffer = await getDefaultOpenGraphImageBuffer();
|
||||
@@ -23,9 +24,14 @@ export const GET: APIRoute<any, Params> = async (context) => {
|
||||
});
|
||||
}
|
||||
|
||||
const svg = await getResourceOpenGraph('guide', slug);
|
||||
const svg = await getResourceOpenGraph(
|
||||
'guide',
|
||||
slug,
|
||||
Object.fromEntries(searchParams.entries()),
|
||||
);
|
||||
const transformer = Transformer.fromSvg(svg).crop(0, 0, 1200, 630);
|
||||
|
||||
// @ts-expect-error - Buffer is not assignable to BodyInit
|
||||
return new Response(await transformer.png(), {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'sql-hard-to-learn';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/sql/hard-to-learn`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner"></div>
|
||||
</BaseLayout>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'sql-how-long-to-learn';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/sql/how-long-to-learn`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot="changelog-banner"></div>
|
||||
</BaseLayout>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'sql-vs-mysql';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/sql/vs-mysql`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import GuideContent from '../../components/Guide/GuideContent.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getGuideById } from '../../lib/guide';
|
||||
import { getOpenGraphImageUrl } from '../../lib/open-graph';
|
||||
|
||||
const guideId = 'sql-vs-python';
|
||||
const guide = await getGuideById(guideId);
|
||||
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/sql/vs-python`}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
<GuideContent guide={guide!} />
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,113 @@
|
||||
import { FetchError, httpGet } from '../lib/query-http';
|
||||
|
||||
export const allowedOfficialGuideStatus = ['draft', 'published'] as const;
|
||||
export type AllowedOfficialGuideStatus =
|
||||
(typeof allowedOfficialGuideStatus)[number];
|
||||
|
||||
export interface OfficialGuideDocument {
|
||||
_id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
content: any;
|
||||
authorId: string;
|
||||
roadmapId?: string;
|
||||
featuredImage?: string;
|
||||
status: AllowedOfficialGuideStatus;
|
||||
publishedAt?: Date;
|
||||
seo?: {
|
||||
metaTitle?: string;
|
||||
metaDescription?: string;
|
||||
keywords?: string[];
|
||||
};
|
||||
tags?: string[];
|
||||
|
||||
questionCount?: number;
|
||||
questionTopicCount?: number;
|
||||
|
||||
viewCount?: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
type ListOfficialGuidesQuery = {
|
||||
authorSlug?: string;
|
||||
roadmapId?: string;
|
||||
};
|
||||
|
||||
export async function listOfficialGuides(query: ListOfficialGuidesQuery = {}) {
|
||||
try {
|
||||
const guides = await httpGet<OfficialGuideDocument[]>(
|
||||
`/v1-list-official-guides`,
|
||||
query,
|
||||
);
|
||||
|
||||
return guides.sort((a, b) => {
|
||||
const aDate = new Date(a.createdAt);
|
||||
const bDate = new Date(b.createdAt);
|
||||
|
||||
return bDate.getTime() - aDate.getTime();
|
||||
});
|
||||
} catch (error) {
|
||||
if (FetchError.isFetchError(error) && error.status === 404) {
|
||||
return [];
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export interface OfficialAuthorDocument {
|
||||
_id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
bio?: string;
|
||||
avatar?: string;
|
||||
socialLinks?: {
|
||||
twitter?: string;
|
||||
linkedin?: string;
|
||||
github?: string;
|
||||
website?: string;
|
||||
};
|
||||
isActive: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
type GuideWithAuthor = OfficialGuideDocument & {
|
||||
author?: Pick<
|
||||
OfficialAuthorDocument,
|
||||
'name' | 'slug' | 'avatar' | 'bio' | 'socialLinks'
|
||||
>;
|
||||
relatedGuides?: Pick<OfficialGuideDocument, 'title' | 'slug' | 'roadmapId'>[];
|
||||
};
|
||||
|
||||
export type OfficialGuideResponse = GuideWithAuthor;
|
||||
|
||||
export async function getOfficialGuide(slug: string, roadmapId?: string) {
|
||||
try {
|
||||
const guide = await httpGet<OfficialGuideResponse>(
|
||||
`/v1-official-guide/${slug}`,
|
||||
{
|
||||
...(roadmapId ? { roadmapId } : {}),
|
||||
},
|
||||
);
|
||||
|
||||
return guide;
|
||||
} catch (error) {
|
||||
if (FetchError.isFetchError(error) && error.status === 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function getOfficialGuideHref(slug: string, roadmapId?: string) {
|
||||
const isExternal = roadmapId && roadmapId !== 'questions';
|
||||
return isExternal
|
||||
? `${import.meta.env.PUBLIC_APP_URL}/${roadmapId}/${slug}`
|
||||
: roadmapId
|
||||
? `/${roadmapId}/${slug}`
|
||||
: `/guides/${slug}`;
|
||||
}
|
||||
Reference in New Issue
Block a user