-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
docs: add HackMD as a CMS guide #14553
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ArmandPhilippot
merged 5 commits into
withastro:main
from
EastSun5566:feat/hackmd-cms-guide
Sep 21, 2026
+228
−0
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
f599858
docs: add HackMD CMS guide
EastSun5566 7137845
docs: address HackMD guide review
EastSun5566 dcea267
docs: use relative link for set html
EastSun5566 e84a853
docs: clarify HackMD publishing
EastSun5566 274cb9f
Merge branch 'main' into feat/hackmd-cms-guide
ArmandPhilippot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'; | ||
| 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. | ||
|
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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.