Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Kanpages

A free, open-source Astro theme for a personal site with four sections:

  • Home: a short intro, three posts, and six notes.
  • Posts: long-form writing, each with a one-sentence summary.
  • Notes: short entries, listed as tight date-and-title rows.
  • Work: bio, experience, selected projects, contact links, and a call to action.

Everything you publish is a markdown file in its own folder, next to its images, video, and anything else it needs. npm run build turns that into a static site you can upload to any host.

The Kanpages demo: the Home page in dark mode, and a post with a diagram in light mode

Features: dark by default with a light toggle · four color palettes · pinning (per section and to Home) · collapsible code blocks · Mermaid diagrams rendered at build time · Chart.js charts · colocated video/audio · YouTube embeds · static search · tags and series · RSS.

Quickstart

npm create astro@latest -- --template bilarikan/astro-theme-kanpages
cd your-project-name
npm install
npm run dev

Requires Node 22.12 or newer. npm install also downloads a headless Chromium (via Playwright), which is used to render Mermaid diagrams at build time.

Then:

  1. Edit kanpages.config.json: site title, your name, the Home intro, and the Work page.
  2. Replace the example entries in src/content/posts/ and src/content/notes/.
  3. Run npm run build. The finished site is in dist/.

Writing

Entries are folders

src/content/
  posts/
    my-first-post/
      index.md        ← the post
      diagram.png     ← images it uses
      walkthrough.mp4 ← video it uses
  notes/
    quick-thought.md  ← a note with no media can be a single file
    another-note/
      index.md

The folder (or file) name becomes the URL: /posts/my-first-post/. Use .mdx instead of .md if you want to use components in an entry.

Frontmatter

---
title: "My first post"
date: 2026-09-29
summary: "One sentence saying what this post is about." # required for posts
tags: [writing, tools]
series: "Getting Started" # posts only; behaves like a tag
pin: true                 # top of its own section
pinHome: true             # on Home, ahead of newer entries
draft: true               # visible in dev, left out of builds
---
Field Posts Notes Notes on use
title required required
date required required YYYY-MM-DD. Every date on the site is shown this way.
summary required optional One sentence. Shown under post titles and used in meta.
tags optional optional Each tag gets a page at /tags/<tag>/.
series optional — Listed on /tags/<series-name>/ alongside tags.
pin optional optional Default false.
pinHome optional optional Default false.
draft optional optional Default false.

Pinning

Two independent flags, both on posts and notes:

  • pin: true floats an entry to the top of /posts/ or /notes/, with a pin marker. There's no limit.
  • pinHome: true puts an entry on Home ahead of the most recent ones. Home shows home.posts posts and home.notes notes (3 and 6 by default). If more entries are pinned than fit, the newest pinned ones win and the build logs a warning.

Pinned entries sort newest-first among themselves. When nothing is pinned, Home shows the most recent entries.

Pagination

/posts/ and /notes/ show pagination.posts (default 10) and pagination.notes (default 30) entries per page. Older entries continue on /posts/page/2/, /posts/page/3/, and so on, with Newer/Older links and page numbers at the bottom. Pinned entries lead page 1 and count toward its size.

Images

Normal markdown with a relative path. Images are optimized at build time.

![Describe the image](./diagram.png)

Video, audio, and YouTube

Use the same image syntax. An optional quoted title becomes a caption.

![What the clip shows](./walkthrough.mp4 "Optional caption")
![Episode 12](./episode.mp3)
![Talk title](https://www.youtube.com/watch?v=VIDEO_ID)

Local video and audio files (.mp4 .webm .mov .m4v .ogv .mp3 .m4a .ogg .wav) are copied next to the built page (dist/posts/my-first-post/walkthrough.mp4) and served from your own host. So are other non-image files in an entry's folder, such as PDFs you link to. YouTube links become a privacy-enhanced (youtube-nocookie.com) embed.

Collapsible code

Mark the lines that should start out collapsed on the code fence itself:

```ts collapse={5-30}
// lines 5–30 are hidden behind "Show 26 lines"
```

Several ranges work too: collapse={1-4, 20-32}. Once expanded, the section shows a "Hide" bar so readers can collapse it again. Blocks without collapse render in full. Code blocks also get syntax highlighting, a copy button, and optional file titles (```ts title="src/app.ts").

Diagrams (Mermaid)

```mermaid caption="Optional caption"
flowchart LR
  A[Write] --> B[Build] --> C[Publish]
```

Diagrams are rendered to SVG at build time, so readers download no JavaScript for them. A light and a dark version are both generated and swap with the site's mode. Diagrams wider than the column are scaled down to fit, and get a View full size button that opens them at their natural size in an overlay (scrollable if needed); closing it returns the reader to the post. The button only appears when a diagram is actually shrunk, and its small script only ships on pages with a diagram.

Charts (Chart.js)

A ```chart block holds a Chart.js configuration as JSON:

```chart caption="Optional caption"
{
  "type": "bar",
  "data": {
    "labels": ["Q1", "Q2", "Q3"],
    "datasets": [{ "label": "Posts", "data": [3, 5, 4] }]
  }
}
```

Series colors come from the active palette unless you set backgroundColor or borderColor yourself. Chart.js is loaded only on pages that contain a chart. Invalid JSON fails the build with the file name.

Config reference

Everything outside the content folders lives in kanpages.config.json. It is validated at build time, so a typo fails the build with a specific message instead of shipping a broken page.

{
  "site": {
    "title": "Your Name",
    "description": "Used for meta tags and RSS.",
    "url": "https://example.com",
    "lang": "en",                  // optional
    "favicon": "/favicon.svg",     // optional
    "ogImage": "/og.png"           // optional, for link previews
  },
  "author": { "name": "Your Name" },
  "nav": [                         // the header links, in order
    { "label": "Home", "href": "/" },
    { "label": "Posts", "href": "/posts/" },
    { "label": "Notes", "href": "/notes/" },
    { "label": "Work", "href": "/work/" }
  ],
  "theme": {
    "palette": "midnight",         // midnight | mono | moss | ember
    "defaultMode": "dark",         // dark | light | system
    "paletteSwitcher": false       // true shows palette swatches in the footer
  },
  "home": {
    "intro": "Raw HTML allowed.",
    "posts": 3,
    "notes": 6
  },
  "pagination": {                  // entries per page on /posts/ and /notes/
    "posts": 10,                   // default 10 (the demo uses 5 so page 2 shows)
    "notes": 30                    // default 30 (the demo uses 8)
  },
  "work": {
    "intro": ["Paragraph one.", "Paragraph two."],
    "experience": [
      { "role": "Role", "org": "Company", "start": "2020", "end": "Present", "summary": "Optional." }
    ],
    "projects": [
      { "title": "Project", "description": "One or two sentences.", "href": "/optional-link/" }
    ],
    "links": [
      { "label": "Email", "text": "you@example.com", "href": "mailto:you@example.com" }
    ],
    "cta": {                       // optional
      "heading": "Work with me",
      "body": "What you offer and who it's for.",
      "label": "Get in touch",
      "href": "mailto:you@example.com"
    }
  },
  "support": {
    "show": false,                 // keep false until you have a real account
    "label": "Pay what you like",
    "href": "https://ko-fi.com/yourname"
  },
  "footer": {
    "note": "Open to consulting — <a href=\"/work/\">see Work</a>.", // raw HTML, "" to remove
    "credit": "Built with Kanpages."                                  // raw HTML, "" to remove
  }
}

When support.show is true, a "Pay what you like" pill links out from under every post and note title. Turn it on while you preview a draft of your site, and back off until there's an account to link to.

Palettes and dark mode

The site loads in defaultMode and the header's sun/moon button switches modes. The visitor's choice is remembered and applied before first paint, so the page never flashes the wrong background.

Palette Look
midnight Deep navy with a purple accent. (default)
mono Black and white, no accent color.
moss Dark green-grey with a mint accent.
ember Warm brown-black with an amber accent.

Every palette has a dark and a light variant. Palettes only change color: the layout, type, and spacing are identical across all four.

To add a palette, add its name to src/lib/palettes.ts and a dark and light block for it in src/styles/palettes.css. Code blocks, charts, and every component read the same --kp-* variables, so nothing else needs to change.

Search

Search is built into the header. Press / or ⌘K to open it. Pagefind indexes your posts and notes right after astro build, and the index ships as plain static files: there's no server, no external service, and nothing to maintain.

Search only works on a built site, so use npm run build && npm run preview to try it locally. npm run dev shows a short explanation instead.

Deploying

npm run build writes a fully static site to dist/.

Shared hosting (cPanel, Apache): upload the contents of dist/ into public_html/, not the dist folder itself. The theme includes a public/.htaccess with ErrorDocument 404 /404.html, so the custom 404 page works without extra setup. Remember to upload .htaccess too; some FTP clients hide dotfiles by default.

Netlify, Vercel, Cloudflare Pages: build command npm run build, output directory dist. The build needs Playwright's Chromium for Mermaid, which npm install fetches automatically. On hosts that skip install scripts, add npx playwright install --only-shell chromium before the build.

Under a sub-path (e.g. example.com/blog/): set Astro's base in astro.config.mjs and pass the same value to the media plugin: [remarkMedia, { base: "/blog/" }].

Architecture, if you're extending this

  • kanpages.config.json is read and validated once, in src/lib/config.ts. Components import the typed config from there.
  • src/content.config.ts defines the posts and notes collections and their frontmatter. src/lib/content.ts holds the queries: sorting, drafts, splitPinned, homeSlice, and tag grouping.
  • Rich content is handled by three remark plugins in src/plugins/ (remark-media, remark-mermaid, remark-chart), which run before Expressive Code highlights the remaining code blocks. Settings for code blocks live in ec.config.mjs.
  • src/integrations/colocated-media.mjs copies non-image files from entry folders into dist/ and serves them in dev.
  • Colors are CSS custom properties (--kp-*) set per palette and mode in src/styles/palettes.css. Components never hardcode a color.

License

MIT. See LICENSE.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages