Markdown Cheat Sheet

Markdown Cheat Sheet

Published
Updated
10 min read

Table of Contents

Bold

**Bold**

Italicized

_Italicized_

Strike Through

The world is flat.

~~The world is flat.~~

Quote

Quote

> Quote

Highlight

I need to highlight these very important words.

I need to highlight these **very important words**.

Subscript

H2O

H<sub>2</sub>O

Superscript

E = MC2

E = MC<sup>2</sup>

Emojis

🛸 🚀 👽 👾 🤖 👨‍🚀

🛸 🚀 👽 👾 🤖 👨‍🚀

Ordered List

  1. First item
    • First sub item
      1. First nested sub item
      2. Second nested sub item
  2. Second item
1. First item
   - First sub item
     1. First nested sub item
     2. Second nested sub item
1. Second item

Unordered List

  • First item
    • First sub item
    1. Second sub item
  • Second item
- First item
  - First sub item
  1. Second sub item
- Second item

Code Line

code

`code`

Code Block

{
  "firstName": "John",
  "lastName": "Smith",
  "age": 25
}
```json
{
  "firstName": "John",
  "lastName": "Smith",
  "age": 25
}
```

Markdown Guide

[Markdown Guide](https://www.markdownguide.org)

Image

astro_logo

![astro_logo](https://kevinc.design/images/tip-markdown-cheat-sheet/astro-dark-logo.webp)

Table

SyntaxDescription
HeaderTitle
ParagraphText
HeaderTitle
ParagraphText
HeaderTitle
ParagraphText
| Syntax    | Description |
| --------- | ----------- |
| Header    | Title       |
| Paragraph | Text        |
| Header    | Title       |
| Paragraph | Text        |
| Header    | Title       |
| Paragraph | Text        |

Task List

  • Check fuel levels and ensure engines are operational.
  • Verify all systems are functioning properly.
  • Initiate launch sequence and monitor ascent.
- [x] Check fuel levels and ensure engines are operational.
- [ ] Verify all systems are functioning properly.
- [ ] Initiate launch sequence and monitor ascent.

Footnote

Here’s a sentence with a footnote. 1

Here's a sentence with a footnote. [^1]
[^1]: This is the footnote.

Horizontal Rule


---

H1

# H1

H2

## H2

H3

### H3

H4

#### H4
H5
##### H5
H6
###### H6

Astro Markdown 🚀

This post is somewhere between a Markdown cheat sheet and a showcase of Markdown styles using Astro and Shiki.

Layout and Syntax Highlighting

Shiki supports syntax highlighting for over 100 different languages! It’s extremely easy to get started by modifying your Astro config.

Astro.config.mjs
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
import tailwind from '@astrojs/tailwind';

export default defineConfig({
  site: 'https://kevinc.design',
  markdown: {
    shikiConfig: {
      theme: 'nord', // add this
      wrap: false, // optionally allow word wrap
    },
  },
  integrations: [sitemap(), tailwind()],
});

Behind the scenes this uses remark provided by @astrojs/markdown-remark which is provided by the base astro package.

There are many different themes in Shiki Astro integration, but the exact keys were kind of difficult to hunt down. I expected they would be in the docs, but I eventually found them by peeking through source. Note that it is also possible to theme with CSS variables as seen here. I may explore this in the future.

// node_modules/@astrojs/markdown-remark/node_modules/shiki/dist/index.d.ts
declare type Theme =
  | 'css-variables'
  | 'dark-plus'
  | 'dracula-soft'
  | 'dracula'
  | 'github-dark-dimmed'
  | 'github-dark'
  | 'github-light'
  | 'hc_light'
  | 'light-plus'
  | 'material-darker'
  | 'material-default'
  | 'material-lighter'
  | 'material-ocean'
  | 'material-palenight'
  | 'min-dark'
  | 'min-light'
  | 'monokai'
  | 'nord'
  | 'one-dark-pro'
  | 'poimandres'
  | 'rose-pine-dawn'
  | 'rose-pine-moon'
  | 'rose-pine'
  | 'slack-dark'
  | 'slack-ochin'
  | 'solarized-dark'
  | 'solarized-light'
  | 'vitesse-dark'
  | 'vitesse-light';

Unfortunately page layout was a bit more of a struggle. I ended up using a heavily customized derivative of github-markdown-css. Many classes were removed or replaced with Tailwind definitions. Even though I’ve cut a lot out, it’s still over 350 lines… a bit too long to post here. Maybe I’ll link it after I make this repo public. 🤞

For future reference in this post, the markdown body class is wrapping the <slot/> of my Layout component like this.

<BaseLayout>
  <article>
    <div class="markdown-body">
      <slot />
    </div>
  </article>
</BaseLayout>

It bothers me that a tags don’t have _blank targets by default. This means that every link takes you away from the site.

To solve this I created a TargetBlank.astro component to inject a _blank target into links queried by selector. This forces links to open in a new tab vs redirecting from this page. The selector ensures only links within paragraphs are altered. This is because links in the TOC or footnotes do not need a target.

TargetBlank.astro
---
const { selector } = Astro.props;
---

<script define:vars={{ selector }}>
  document.addEventListener('DOMContentLoaded', () => {
    document.querySelectorAll(selector).forEach((link) => {
      link.target = '_blank';
    });
  });
</script>
Usage in Layout.astro
<BaseLayout>
  <TargetBlank selector="article p > a" />
  <article>...</article>
</BaseLayout>

Code Block Modifications

Code blocks would not be complete without a copy button. Let’s create a component for that!

PreCopy.astro
---
---

<script>
  const buttonText =
    '<span class="fade-in mb-0 mr-2">Copy</span><i class="mb-0 fade-in fa-solid fa-satellite-dish"></i>';

  document.addEventListener('DOMContentLoaded', () => {
    const blocks = document.querySelectorAll('pre.astro-code');

    blocks.forEach((block: HTMLElement) => {
      if (!navigator.clipboard) return;

      const button = document.createElement('div');
      button.classList.add('copy-button');
      button.innerHTML = buttonText;
      block.appendChild(button);

      button.addEventListener('click', async () => {
        await copy(block, button);
      });
    });
  });

  const copy = async (block: HTMLElement, button: HTMLElement) => {
    await navigator.clipboard.writeText(block?.innerText);

    button.innerHTML =
      '<span class="fade-in text-success mb-0 mr-2">Copied</span><i class="mb-0 fade-in fa-solid fa-satellite-dish text-success"></i>';

    setTimeout(() => {
      button.innerHTML = buttonText;
    }, 2000);
  };
</script>

This component works well enough, but I think the implementation could be improved. It seems messy. Again, we will leave this as a future item. Sometimes you just have to get it done. All that’s left is to add some style before calling the component in our layout.

pre.astro-code {
  @apply relative;
}

pre.astro-code > .copy-button {
  @apply absolute top-2 right-2 rounded bg-base-100 px-2 shadow-md hover:cursor-pointer hover:bg-base-300;
}

.fade-in {
  animation: fade-in 2s;
}

@keyframes fade-in {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}
Layout.astro
<BaseLayout>
  <TargetBlank selector="article p > a" />
  <PreCopy />
  <article>...</article>
</BaseLayout>

Next up on my wishlist was a Table of Contents and header hyperlinks. Luckily there are a few remark and rehype plugins to help out with this. Start by installing the necessary dependencies and then modify Astro config to follow suit.

npm i -D remark-toc rehype-slug rehype-autolink-headings
Astro.config.mjs
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
import tailwind from '@astrojs/tailwind';
import remarkToc from 'remark-toc';
import { rehypeHeadingIds } from '@astrojs/markdown-remark';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';

export default defineConfig({
  site: 'https://kevinc.design',
  markdown: {
    shikiConfig: {
      theme: 'nord',
      wrap: false,
    },
    // add remark and rehype plugin configuration
    remarkPlugins: [[remarkToc, { heading: 'contents' }]],
    rehypePlugins: [
      rehypeHeadingIds,
      rehypeSlug,
      [
        rehypeAutolinkHeadings,
        {
          behavior: 'append',
        },
      ],
      ['rehype-toc', { headings: ['h1', 'h2', 'h3', 'h4'] }],
    ],
  },
  integrations: [sitemap(), tailwind()],
});

This will append an a tag to the specified headings as well as auto generate a ToC for every post. Unfortunately this is only the first part of the battle. The ToC still needs to be styled and another rule is needed to offset scroll so it doesn’t overlap with the fixed header.

.markdown-body * {
  scroll-margin-top: 6rem;
}

.markdown-body > nav {
  @apply ml-4;
}

.markdown-body > nav.toc ol {
  @apply m-0 ml-4 p-0;
}

.markdown-body > nav.toc ol > .toc-item {
  @apply ml-2 list-disc !important;
}

.markdown-body > nav.toc .toc-level {
  @apply ml-2;
}

Finally we need to fill in the content of each header link and copy the url to system clipboard on click. I created another script only component to handle this task and inject it into the layout.

HeaderLink.astro
---
---

<script>
  document.addEventListener('DOMContentLoaded', () => {
    const headerIcons = document.querySelectorAll('span.icon.icon-link');
    headerIcons.forEach((icon) => {
      // don't append copy links to these headers
      switch (icon.parentElement?.parentElement?.tagName) {
        case 'H5':
          return;
        case 'H6':
          return;
        default:
          break;
      }

      icon.innerHTML =
        '<i class="fa-solid fa-satellite-dish text-xl text-white opacity-50 hover:opacity-100"></i>';

      // copy header url to clipboard
      icon.addEventListener('click', async (e) => {
        const a = icon.parentElement as HTMLAnchorElement;
        const t = e.target as HTMLAnchorElement;
        await navigator.clipboard.writeText(a.href);
        t.classList.remove('text-white');
        t.classList.add('text-success');
        t.innerHTML =
          '<span class="fade-in ml-1 font-normal text-success">... Copied</span>';
        setTimeout(() => {
          t.innerText = '';
          t.classList.remove('text-success');
          t.classList.add('text-white');
        }, 2000);
      });
    });
  });
</script>
Layout.astro
<BaseLayout>
  <TargetBlank selector="article p > a" />
  <PreCopy />
  <article>...</article>
</BaseLayout>

So let’s say that implementation isn’t good enough. Maybe a little too ‘pluginey’ for your tastes. What I found was that header elements were not wrapped by any container. Therefore pointer-cursor was applied to either just the icon or the whole bar. This is not ideal and any work around seemed hacky and the UI was still a bit clunky. Ultimately I did away with the plugins and rolled my own by refactoring HeaderLink.astro

There were a couple of notable things here.

  1. The not all headers need links.
    • This was pretty easy to resolve because id is added to each Markdown header. I’m not using id in other headers.
    • Wrapping each header in a flex box keeps layout while allowing better cursor behavior.
  2. I had to query headers twice to regain the reference after wrapping them with a container.
    • The reference was lost after wrapping and the event handler was not loaded / was lost.
  3. Because there are no a tags we need to change the page url manually.
    • window.history.pushState() is perfect for this.
  4. The resulting effect of the header click is a bit different because there is no icon and we are fading header text.
HeaderLink.astro
---
---

<script>
  const selector = 'h1,h2,h3,h4,h5,h6';

  document.addEventListener('DOMContentLoaded', () => {
    let headers = document.querySelectorAll(selector);

    for (const header of headers) {
      if (!header.id) continue; // skip non markdown headers
      wrapHeader(header);
    }

    headers = document.querySelectorAll(selector); // refresh

    for (const header of headers) {
      if (!header.id) continue;
      registerHeaderClick(header);
    }
  });

  const wrapHeader = (header: Element) => {
    // justify start for smaller headers, center for bigger ones
    const justify =
      header.tagName == 'H4' || header.tagName == 'H5' || header.tagName == 'H6'
        ? 'justify-start'
        : 'justify-center';

    header.classList.add('cursor-pointer');
    header.classList.add('hover:text-white');
    header.outerHTML = `<div class="flex items-center ${justify}">${header.outerHTML}</div>`;
  };

  const registerHeaderClick = (header: Element) => {
    header.addEventListener('click', async () => {
      header.scrollIntoView({
        behavior: 'smooth',
        block: 'start',
        inline: 'nearest',
      });

      const url =
        window.location.origin + window.location.pathname + '#' + header.id;

      window.history.pushState(null, '', url);

      await navigator.clipboard.writeText(url);

      const preHTML = header.innerHTML;

      header.classList.remove('hover:text-white');
      header.classList.remove('fade-in');
      header.classList.add('text-success');
      header.innerHTML = 'Copied Link';

      setTimeout(() => {
        header.innerHTML = preHTML;
        header.classList.remove('text-success');
        header.classList.add('hover:text-white');
        header.classList.add('fade-in');
      }, 2000);
    });
  };
</script>
Astro.config.mjs
export default defineConfig({
  site: 'https://kevinc.design',
  markdown: {
    shikiConfig: {
      theme: 'nord',
      wrap: false,
    },
    // remove unused plugins leaving just these
    remarkPlugins: [[remarkToc, { heading: 'contents' }]],
    rehypePlugins: [
      rehypeHeadingIds,
      ['rehype-toc', { headings: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] }],
    ],
  },
  integrations: [sitemap(), tailwind()],
});

Conclusion

All of these modifications are demonstrated by this page. They apply to every article on this site. In conclusion, Shiki with Astro and Markdown is a powerhouse combination for anyone looking to enhance their site. Safe travels!

Footnotes

  1. This is the footnote.