Add table of contents to guides

pull/5465/head
Kamran Ahmed 7 months ago
parent 852622f5ac
commit 60568caff7
  1. 61
      src/components/Guide/GuideContent.astro
  2. 2
      src/components/GuideHeader.astro
  3. 2
      src/components/MarkdownFile.astro
  4. 98
      src/components/TableOfContent/TableOfContent.tsx
  5. 14
      src/data/guides/backend-languages.md
  6. 30
      src/lib/guide.ts
  7. 11
      src/pages/backend/developer-skills.astro
  8. 11
      src/pages/backend/developer-tools.astro
  9. 9
      src/pages/backend/languages.astro
  10. 9
      src/pages/guides/[guideId].astro

@ -0,0 +1,61 @@
---
import { getGuideTableOfContent, type GuideFileType } from '../../lib/guide';
import MarkdownFile from '../MarkdownFile.astro';
import { TableOfContent } from '../TableOfContent/TableOfContent';
interface Props {
guide: GuideFileType;
}
const { guide } = Astro.props;
const allHeadings = guide.getHeadings();
const tableOfContent = getGuideTableOfContent(allHeadings);
const showTableOfContent = tableOfContent.length > 0;
const { frontmatter: guideFrontmatter, author } = guide;
---
<article class='lg:grid lg:max-w-full lg:grid-cols-[1fr_minmax(0,700px)_1fr]'>
{
showTableOfContent && (
<div class='bg-gradient-to-r from-gray-50 py-0 lg:col-start-3 lg:col-end-4 lg:row-start-1'>
<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='text-balance text-4xl mb-3 font-bold'>{guideFrontmatter.title}</h1>
<p
class='flex items-center justify-start text-sm text-gray-400 my-0'
>
<a
href={`/authors/${author.id}`}
class='inline-flex items-center font-medium hover:text-gray-600 hover:underline underline-offset-2'
>
<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'>&middot;</span>
<a
class='hover:text-gray-600 underline-offset-2 hidden 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>

@ -7,6 +7,8 @@ export interface Props {
const { guide } = Astro.props;
const { frontmatter, author } = guide;
return undefined;
---
<div class='border-b bg-white py-5 sm:py-12'>

@ -1,5 +1,5 @@
<div
class='prose-xl prose-blockquote:font-normal prose container prose-code:bg-transparent prose-h2:text-3xl prose-h2:mt-10 prose-h2:mb-3 prose-h5:font-medium prose-h3:mt-2 prose-img:mt-1'
class='container prose-h2:text-balance prose-h3:text-balance prose-h4:text-balance prose-h5:text-balance prose prose-xl prose-h2:mb-3 prose-h2:mt-10 prose-h2:scroll-mt-5 prose-h2:text-3xl prose-h3:mt-2 prose-h3:scroll-mt-5 prose-h5:font-medium prose-blockquote:font-normal prose-code:bg-transparent prose-img:mt-1 prose-h2:sm:scroll-mt-10 prose-h3:sm:scroll-mt-10'
>
<slot />
</div>

@ -0,0 +1,98 @@
import { useState, type CSSProperties } from 'react';
import type { HeadingGroupType } from '../../lib/guide';
import { ChevronDown } from 'lucide-react';
import { cn } from '../../lib/classname';
type TableOfContentProps = {
toc: HeadingGroupType[];
};
export function TableOfContent(props: TableOfContentProps) {
const { toc } = props;
const [isOpen, setIsOpen] = useState(false);
if (toc.length === 0) {
return null;
}
const totalRows = toc.flatMap((heading) => {
return [heading, ...heading.children];
}).length;
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-10',
{
'top-0 lg:sticky': totalRows <= 20,
},
)}
>
<h4 className="text-lg font-medium max-lg:hidden">In this article</h4>
<button
className="flex w-full items-center justify-between gap-2 bg-gray-300 px-3 py-2 text-sm font-medium lg:hidden"
onClick={() => setIsOpen(!isOpen)}
>
Table of Contents
<ChevronDown
size={16}
className={cn(
'transform transition-transform',
isOpen && 'rotate-180',
)}
/>
</button>
<ol
className={cn(
'mt-2 space-y-1 max-lg:absolute max-lg:top-full max-lg:mt-0 max-lg:w-full max-lg:space-y-0 max-lg:bg-white max-lg:shadow',
!isOpen && 'hidden lg:block',
isOpen && 'block',
)}
>
{toc.map((heading) => (
<li key={heading.slug}>
<a
href={`#${heading.slug}`}
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;
}
setIsOpen(false);
}}
>
{heading.text}
</a>
{heading.children.length > 0 && (
<ol className="my-0 ml-4 mt-1 space-y-1 max-lg:ml-0 max-lg:mt-0 max-lg:list-none max-lg:space-y-0">
{heading.children.map((children) => {
return (
<li key={children.slug}>
<a
href={`#${children.slug}`}
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 max-lg:pl-8"
onClick={() => {
if (!isOpen) {
return;
}
setIsOpen(false);
}}
>
{children.text}
</a>
</li>
);
})}
</ol>
)}
</li>
))}
</ol>
</div>
);
}

@ -299,7 +299,7 @@ Let’s take a look at some pros and cons for the last programming language on o
- Go’s ecosystem is quite young when compared to the other alternatives here, so the maturity of the tools available might not be the same as, for example, Java or JavaScript tooling.
### Choosing the Ideal Backend Language
## Choosing the Ideal Backend Language
So, are these the best backend programming languages out there? Is there an absolute “best” backend programming language?
@ -326,13 +326,13 @@ A handy tool when trying to evaluate a language like that is [roadmap.sh](https:
There you’ll find community-maintained roadmaps for many career paths within software development. In particular, for this article, the [backend roadmap](https://roadmap.sh/backend) is a great place to start, because while picking a backend language is important, you’ll see there that it’s not just about the language. In fact, there is a lot of tech around the language that is also required (I’m referring to databases, git, understanding how client-server communication works, and a big “etc).
### Jumpstarting Your Backend Development Journey
## Jumpstarting Your Backend Development Journey
To get started with your backend development journey, it's crucial to have a roadmap that guides you through the learning process and equips you with the skills to build robust and scalable backend systems.
Lucky for you, if you’re reading this, that means you’ve found the most complete and comprehensive roadmap online: [roadmap.sh](https://roadmap.sh), the current [backend roadmap](https://roadmap.sh/backend) is filled with details of everything you should and could (optionally) learn in your journey to becoming a backend developer.
### Guided Learning: From Online Courses to Bootcamps
## Guided Learning: From Online Courses to Bootcamps
Online courses and bootcamps serve as invaluable companions on your learning expedition. Platforms like Udemy, Coursera, and freeCodeCamp offer comprehensive backend development courses.
@ -340,13 +340,13 @@ These resources not only cover programming languages like Python, Java, or JavaS
Whatever choice you go for, make sure you’re not following trends or just copying the learning methods of others. Learning is a very personal experience and what works for others might not work for you, and vice versa. So make sure to do the proper research and figure out what option works best for you.
### Building Community Connections for Learning Support
## Building Community Connections for Learning Support
Joining developer communities (there are several on Twitter for example), forums like Stack Overflow, or participating in social media groups dedicated to backend development creates a network of support.
Engaging with experienced developers, sharing challenges, and seeking advice fosters a collaborative learning environment. Attend local meetups or virtual events if you can to connect with professionals in the field, gaining insights and building relationships that can prove invaluable throughout your journey.
### Think about you and your project
## Think about you and your project
There are many ways to go about picking the ideal backend language for you. If there is anything you should take home with you after reading this article, it is that most languages are equivalent in the sense that you’ll be able to do pretty much everything with any of them.
@ -359,7 +359,7 @@ The questions you should also be asking yourself are:
In the end, personal preference and actual project requirements (if you have any) are very important, because both will influence how much you enjoy (or don’t enjoy) the learning process.
### Crafting a Portfolio to Display Your Backend Skills:
## Crafting a Portfolio to Display Your Backend Skills:
As you accumulate skills and knowledge, showcase your journey through a well-crafted portfolio. Include projects that highlight your backend skills, demonstrating your ability to - design databases, implement server-side logic, and integrate with client side technologies. Whether it's a dynamic web application, a RESTful API, or a data-driven project, your portfolio becomes a tangible representation of your backend development capabilities for potential employers or collaborators.
@ -367,7 +367,7 @@ When it comes to deciding where to publish this portfolio, you have some options
In the end, the important thing is that you should be sharing your experience somewhere, especially when you don’t have working experience in the field.
### Conclusion
## Conclusion
In the end, there are many backend programming languages to choose from, and what language you go for, is up to you and your particular context/needs. All I can do is guide you to the door, but you have to cross it yourself. Some interesting options are:

@ -87,3 +87,33 @@ export async function getGuideById(
return allGuides.find((guide) => guide.id === id);
}
type HeadingType = ReturnType<MarkdownFileType['getHeadings']>[number];
export type HeadingGroupType = HeadingType & { children: HeadingType[] };
const NUMBERED_LIST_REGEX = /^\d+\.\s+?/;
export function getGuideTableOfContent(headings: HeadingType[]) {
const tableOfContents: HeadingGroupType[] = [];
let currentGroup: HeadingGroupType | null = null;
headings
.filter((heading) => heading.depth !== 1)
.forEach((heading) => {
if (heading.depth === 2) {
currentGroup = {
...heading,
text: heading.text.replace(NUMBERED_LIST_REGEX, ''),
children: [],
};
tableOfContents.push(currentGroup);
} else if (currentGroup && heading.depth === 3) {
currentGroup.children.push({
...heading,
text: heading.text.replace(NUMBERED_LIST_REGEX, ''),
});
}
});
return tableOfContents;
}

@ -1,6 +1,6 @@
---
import GuideContent from '../../components/Guide/GuideContent.astro';
import GuideHeader from '../../components/GuideHeader.astro';
import MarkdownFile from '../../components/MarkdownFile.astro';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getGuideById } from '../../lib/guide';
import { getOpenGraphImageUrl } from '../../lib/open-graph';
@ -23,11 +23,6 @@ const ogImageUrl = getOpenGraphImageUrl({
canonicalUrl={guideData.canonicalUrl}
ogImageUrl={ogImageUrl}
>
<GuideHeader guide={guide} />
<div class='mx-auto max-w-[700px] py-5 sm:py-10'>
<MarkdownFile>
<guide.Content />
</MarkdownFile>
</div>
<GuideHeader guide={guide!} />
<GuideContent guide={guide!} />
</BaseLayout>

@ -1,6 +1,6 @@
---
import GuideContent from '../../components/Guide/GuideContent.astro';
import GuideHeader from '../../components/GuideHeader.astro';
import MarkdownFile from '../../components/MarkdownFile.astro';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getGuideById } from '../../lib/guide';
import { getOpenGraphImageUrl } from '../../lib/open-graph';
@ -23,11 +23,6 @@ const ogImageUrl = getOpenGraphImageUrl({
canonicalUrl={guideData.canonicalUrl}
ogImageUrl={ogImageUrl}
>
<GuideHeader guide={guide} />
<div class='mx-auto max-w-[700px] py-5 sm:py-10'>
<MarkdownFile>
<guide.Content />
</MarkdownFile>
</div>
<GuideHeader guide={guide!} />
<GuideContent guide={guide!} />
</BaseLayout>

@ -1,6 +1,6 @@
---
import GuideContent from '../../components/Guide/GuideContent.astro';
import GuideHeader from '../../components/GuideHeader.astro';
import MarkdownFile from '../../components/MarkdownFile.astro';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getGuideById } from '../../lib/guide';
import { getOpenGraphImageUrl } from '../../lib/open-graph';
@ -24,10 +24,5 @@ const ogImageUrl = getOpenGraphImageUrl({
ogImageUrl={ogImageUrl}
>
<GuideHeader guide={guide!} />
<div class='mx-auto max-w-[700px] py-5 sm:py-10'>
<MarkdownFile>
<guide.Content />
</MarkdownFile>
</div>
<GuideContent guide={guide!} />
</BaseLayout>

@ -1,6 +1,6 @@
---
import GuideContent from '../../components/Guide/GuideContent.astro';
import GuideHeader from '../../components/GuideHeader.astro';
import MarkdownFile from '../../components/MarkdownFile.astro';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getAllGuides, type GuideFileType } from '../../lib/guide';
import { getOpenGraphImageUrl } from '../../lib/open-graph';
@ -38,10 +38,5 @@ const ogImageUrl = getOpenGraphImageUrl({
ogImageUrl={ogImageUrl}
>
<GuideHeader guide={guide} />
<div class='mx-auto max-w-[700px] py-5 sm:py-10'>
<MarkdownFile>
<guide.Content />
</MarkdownFile>
</div>
<GuideContent guide={guide!} />
</BaseLayout>

Loading…
Cancel
Save