<main id="app" class="wrapper">
	<aside id="aside" class="sticky top-0 ml-8 pt-16">
				
	</aside>
	<div class="max-w-2xl content mt-32" id="content">
</main>

html {
	height: 100%;
}


body {
	display: flex;
	flex-direction: column;
	margin: auto;
  background-color: white;
	font-family: 'Cabin', sans-serif;
  font-weight: 400;
  line-height: 1.65;
  color: #333;
	font-size: 20px;
	
	&::before {
		content: "";
		position: fixed;
		top: 0; 
		left: 0;
		width: 100%; 
		height: 100%;  
		opacity: .05; 
		z-index: -1;
		background: url("https://static.thenounproject.com/png/2590971-200.png");
		background-size: 200px 200px;
		background-repeat: repeat;
		background-position: center center;
	}
}


h1, h2, h3, h4, h5 {
	font-family: 'Oswald', sans-serif;
  margin: 2.75rem 0 1.05rem;
  font-weight: 400;
  line-height: 1.15;
}

h1 {
  font-size: 3.052em;
}

h2 {font-size: 2.441em;}

h3 {font-size: 1.953em;}

h4 {font-size: 1.563em;}

h5 {font-size: 1.25em;}

.wrapper {
	margin: 0 auto;
	display: grid;
  padding: 10px;
  grid-template-columns: 300px auto;
  grid-gap: 30px;
	align-items: flex-start;
}

.is-active {
	font-weight: 700;
	color: #bb2205;
}
View Compiled

async function fetchAndParseMarkdown() {
  const url = 'https://gist.githubusercontent.com/lisilinhart/e9dcf5298adff7c2c2a4da9ce2a3db3f/raw/2f1a0d47eba64756c22460b5d2919d45d8118d42/red_panda.md'
  const response = await fetch(url)
  const data = await response.text()
  const htmlFromMarkdown = marked(data, { sanitize: true });
  return htmlFromMarkdown
}


function generateLinkMarkup($headings) {
	console.log($headings)
	const parsedHeadings = $headings.map(heading => {
		return {
			title: heading.innerText,
			depth: heading.nodeName.replace(/\D/g,''),
			id: heading.getAttribute('id')
		}
	})
	const htmlMarkup = parsedHeadings.map(h => `
	<li class="${h.depth > 1 ? 'pl-4' : ''}">
		<a href="#${h.id}">${h.title}</a>
	</li>
	`)
	const finalMarkup = `
		<ul>${htmlMarkup.join('')}</ul>
	`
	return finalMarkup
}

function updateLinks(visibleId, $links) {
	$links.map(link => {
		let href = link.getAttribute('href')
		link.classList.remove('is-active')
		if(href === visibleId) link.classList.add('is-active')
	})
}

function handleObserver(entries, observer, $links) {
	entries.forEach((entry)=> {
		const { target, isIntersecting, intersectionRatio } = entry
			if (isIntersecting && intersectionRatio >= 1) {
				const visibleId = `#${target.getAttribute('id')}`
				updateLinks(visibleId, $links)
			}
	})
}

function createObserver($links) {
	const options = {
			rootMargin: "0px 0px -200px 0px",
      threshold: 1
	}
	const callback = (e, o) => handleObserver(e, o, $links)
	return new IntersectionObserver(callback, options)
}

async function init() {
	// Part 1
	const $main = document.querySelector('#content');
	const $aside = document.querySelector('#aside');
	const htmlContent = await fetchAndParseMarkdown();
	$main.innerHTML = htmlContent
	
	// Part 2
	const $headings = [...$main.querySelectorAll('h1, h2')];
	const linkHtml = generateLinkMarkup($headings);
	$aside.innerHTML = linkHtml	
	
	// Part 3
	const motionQuery = window.matchMedia('(prefers-reduced-motion)')
	const $links = [...$aside.querySelectorAll('a')]
	const observer = createObserver($links)
	$headings.map(heading => observer.observe(heading))
}

init();
View Compiled

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/1.9.1/tailwind.min.css
  2. https://fonts.googleapis.com/css?family=Cabin:400,500,600,700
  3. https://fonts.googleapis.com/css?family=Oswald:200,300,400,500,600,700

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/marked/1.1.1/marked.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js