Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions public/logos/hackmd.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
218 changes: 218 additions & 0 deletions src/content/docs/en/guides/cms/hackmd.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
---
title: HackMD & Astro
description: Add content to your Astro project using HackMD as a CMS
sidebar:
label: HackMD
type: cms
stub: false
logo: hackmd
i18nReady: true
---

import { FileTree } from '@astrojs/starlight/components';
import PackageManagerTabs from '~/components/tabs/PackageManagerTabs.astro';
Comment thread
ArmandPhilippot marked this conversation as resolved.
import ReadMore from '~/components/ReadMore.astro';

[HackMD](https://hackmd.io/) is a collaborative Markdown editor and publishing platform. You can use its API to manage your content in HackMD and display it in your Astro project.

## Integrating with Astro

This guide uses the official [`@hackmd/api`](https://github.com/hackmdio/api-client) client to fetch your notes and [`markdown-it`](https://github.com/markdown-it/markdown-it) to render Markdown content.

### Prerequisites

To get started, you will need:

1. **An Astro project** - If you don't have an Astro project yet, the [installation guide](/en/install-and-setup/) will get you up and running.
2. **A HackMD account** - You can [sign up for free](https://hackmd.io/join).
3. **A HackMD access token** - Create one from the API section of your [HackMD settings](https://hackmd.io/settings#api).
4. **At least one publicly readable note** - Set the note's read permission to **Everyone** so the example can safely publish it on your site.
Comment thread
ArmandPhilippot marked this conversation as resolved.

### Setting up credentials

Create a `.env` file in the root of your project and add your HackMD access token:

```ini title=".env"
HACKMD_API_ACCESS_TOKEN=<YOUR_ACCESS_TOKEN>
```

Do not prefix this variable with `PUBLIC_`. This keeps the token available only to your server-side code and prevents Astro from exposing it to the browser.

<ReadMore>Read more about [environment variables](/en/guides/environment-variables/) and `.env` files in Astro.</ReadMore>

### Installing dependencies

Install the HackMD API client and Markdown renderer:

<PackageManagerTabs>
<Fragment slot="npm">
```shell
npm install @hackmd/api markdown-it
```
</Fragment>
<Fragment slot="pnpm">
```shell
pnpm add @hackmd/api markdown-it
```
</Fragment>
<Fragment slot="yarn">
```shell
yarn add @hackmd/api markdown-it
```
</Fragment>
</PackageManagerTabs>

### Configuring HackMD

Create a `hackmd.ts` file in a new `src/lib/` directory. This file initializes the API client, renders Markdown, and creates a URL-friendly identifier for each note:

```ts title="src/lib/hackmd.ts"
import { API } from '@hackmd/api';
import MarkdownIt from 'markdown-it';

export const client = new API(import.meta.env.HACKMD_API_ACCESS_TOKEN);

const md = new MarkdownIt({
html: false,
linkify: true,
typographer: true,
});

export function renderMarkdown(content: string) {
return md.render(content);
}

export function getNoteSlug(note: { permalink: string | null; shortId: string }) {
return note.permalink ?? note.shortId;
}
```

The `html: false` option prevents raw HTML in a note from being passed directly to your generated page. Standard Markdown is still rendered as HTML.

Your project will use the following files:

<FileTree title="Project Structure">
- src/
- lib/
- **hackmd.ts**
- pages/
- **index.astro**
- notes/
- **[slug].astro**
- **.env**
- astro.config.mjs
- package.json
</FileTree>

## Making a blog with Astro and HackMD

This example creates an index of publicly readable notes and a statically generated page for each note.

### Displaying a list of notes

Use `getNoteList()` in `src/pages/index.astro` to retrieve your notes. Filter the results so that only notes with the `guest` read permission are included in the public site:

```astro title="src/pages/index.astro"
---
import { client, getNoteSlug } from '../lib/hackmd';

const notes = await client.getNoteList();
const publicNotes = notes.filter((note) => note.readPermission === 'guest');
---

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>Astro + HackMD</title>
</head>
<body>
<main>
<h1>My HackMD notes</h1>
<ul>
{
publicNotes.map((note) => (
<li>
<a href={`/notes/${getNoteSlug(note)}/`}>{note.title}</a>
</li>
))
}
</ul>
</main>
</body>
</html>
```

:::caution
The access token can read private notes in your account. Keep the `guest` permission filter unless you intentionally want to include other notes in the generated site.
:::

### Generating note pages

Create `src/pages/notes/[slug].astro` to generate a static page for every public note. The note list provides the route and note ID, then `getNote()` retrieves the full Markdown content for that page:

```astro title="src/pages/notes/[slug].astro"
---
import { client, getNoteSlug, renderMarkdown } from '../../lib/hackmd';

export async function getStaticPaths() {
const notes = await client.getNoteList();

return notes
.filter((note) => note.readPermission === 'guest')
.map((note) => ({
params: { slug: getNoteSlug(note) },
props: { noteId: note.id },
}));
}

interface Props {
noteId: string;
}

const { noteId } = Astro.props;
const note = await client.getNote(noteId);
const content = renderMarkdown(note.content);
---

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{note.title}</title>
</head>
<body>
<main>
<article>
<Fragment set:html={content} />
</article>
</main>
</body>
</html>
```

:::caution
Astro's [`set:html` directive](/en/reference/directives-reference/#sethtml) inserts an HTML string without escaping it. This example first passes the note through `markdown-it` with raw HTML disabled. If you enable the `html` option for trusted authors, sanitize the rendered result before passing it to `set:html`.
:::

### Supporting more HackMD syntax

HackMD uses `markdown-it` with extensions for features such as task lists, footnotes, containers, and a table of contents. The minimal configuration above handles standard Markdown. Install only the [`markdown-it` plugins](https://www.npmjs.com/search?q=keywords%3Amarkdown-it-plugin) required by your notes.

### Publishing your site

To deploy your website, visit our [deployment guides](/en/guides/deploy/) and follow the instructions for your preferred hosting provider.

If your project uses Astro's default static mode, you must run a new build to publish changes made in HackMD. If your hosting provider supports it, you can use its webhook function to automatically trigger a new build when HackMD sends a [webhook event](https://hackmd.io/@docs/webhooks-events).

## Official Resources

- [HackMD API documentation](https://hackmd.io/@docs/developer-portal)
- [HackMD OpenAPI documentation](https://api.hackmd.io/v1/docs)

## Community Resources

- [`daily-oops`](https://github.com/Yukaii/daily-oops) - A blog that uses HackMD as its CMS
- [`astro-hackmd`](https://github.com/EastSun5566/astro-hackmd) - A minimal Astro site that uses HackMD as its CMS
1 change: 1 addition & 0 deletions src/data/logos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export const logos = LogoCheck({
gitlab: { file: 'gitlab.svg' },
'google-cloud': { file: 'google-cloud.svg', padding: '.1875em' },
gridsome: { file: 'gridsome.svg', padding: '.15em' },
hackmd: { file: 'hackmd.svg', padding: '0' },
hashnode: { file: 'hashnode.png', padding: '.1875em' },
heroku: { file: 'heroku.svg', padding: '.25em' },
hostinger: { file: 'hostinger.svg', padding: '.2em' },
Expand Down
Loading