Pipitone Labs

Back

From Waline to Giscus: Migrating My Blog's Comment SystemBlur image

This fantastic Astro template came with Waline pre-configured using sample data that was being pulled from a developer’s sandbox. It’s a solid, feature-rich comment system that gave me full control over my data by utilizing the LeanCloud platform. But after thinking a bit more, I started asking myself: Do I really need to run a separate backend and an additional Vercel deployment just for comments?

The answer, it turns out, was no. So I migrated to Giscus, and it was fairly straight-forward.

Why I made the switch#

Waline is genuinely impressive. It supports multiple databases, has a great UI and offers features like emoji reactions and comment management. But the deployment seemed more like a burden than a benefit for my use case. I also wanted to utilize a solution that was US-based.

The maintenance overhead and complexity was a bit much:

  • I had to keep a Vercel deployment running
  • LeanCloud management added another layer of complexity
  • Updates required me to redeploy the backend separately from my blog
  • Cost considerations for the database hosting

Then I discovered Giscus. It uses GitHub Discussions as the backend, which means:

  • Zero infrastructure to maintain - GitHub handles everything
  • Native GitHub integration - readers can use their existing GitHub accounts
  • Markdown support out of the box - code blocks, formatting, etc
  • Completely free - no database costs

For a technical blog where it’s safe to say that most readers have GitHub accounts, this was a no-brainer.

Prerequisites#

I needed the following:

  1. A public GitHub repository for your blog
  2. The Giscus GitHub App installed on that repository
  3. GitHub Discussions enabled on the repository

Step 1: Set Up Giscus on GitHub#

I went over to giscus.app and configured my settings. I needed to:

  1. Enter my repo name (e.g., yourusername/blog)
  2. Choose a discussion category (I created a dedicated “Comments” category)
  3. Select my mapping preference (I use pathname so each page gets its own discussion)
  4. Pick my theme and features

The site will generate all the values I need. I noted the IDs and saved them for later.

Step 2: Create the Giscus component#

Here’s where the actual code comes in. I created a new Astro component for Giscus at src/components/giscus/Comment.astro:

---
import config from '@/site-config'
import { cn } from 'astro-pure/utils'

const { class: className } = Astro.props
---

{
  config.integ.giscus.enable && (
    <div id="giscus-container" class={cn('not-prose', className)}>
      <script is:inline
        src="https://giscus.app/client.js"
        data-repo={config.integ.giscus.repo}
        data-repo-id={config.integ.giscus.repoId}
        data-category={config.integ.giscus.category}
        data-category-id={config.integ.giscus.categoryId}
        data-mapping={config.integ.giscus.mapping}
        data-reactions-enabled={config.integ.giscus.reactionsEnabled ? '1' : '0'}
        data-emit-metadata={config.integ.giscus.emitMetadata ? '1' : '0'}
        data-input-position={config.integ.giscus.inputPosition}
        data-theme={config.integ.giscus.theme}
        data-lang={config.integ.giscus.lang}
        data-loading="lazy"
        crossorigin="anonymous"
        data-strict={config.integ.giscus.strict}
        async
      ></script>
    </div>
  )
}

<style>
  #giscus-container {
    width: 100%;
  }
</style>
astro

The key things happening here:

  • I’m pulling configuration from a centralized config file (more on that next)
  • The not-prose class prevents Tailwind Typography styles from messing with the widget
  • Lazy loading keeps initial page loads fast by loading content as the user scrolls down
  • The component only renders if Giscus is enabled in config

I exported the component - created an index.ts file:

export { default as Comment } from './Comment.astro'
astro

Step 3: Configure site settings#

This is where all the values from giscus.app go. In my site.config.ts, I added the Giscus configuration:

export const integ: IntegrationUserConfig = {
  // ... other integrations ...

  // Comment system
  giscus: {
    enable: true,
    // GitHub repository for comments (format: owner/repo)
    repo: 'yourusername/your-repo',
    // Repository ID - get this from giscus.app
    repoId: 'R_kgDOxxxxxxx',
    // Discussion category
    category: 'General',
    // Category ID - get this from giscus.app
    categoryId: 'DIC_kwDOxxxxxxx',
    // Mapping between the page and the discussion
    mapping: 'pathname',
    // Enable reactions
    reactionsEnabled: true,
    // Emit metadata
    emitMetadata: false,
    // Where to put the comment input box
    inputPosition: 'top',
    // Theme - can be 'light', 'dark', or 'preferred_color_scheme'
    theme: 'dark',
    // Language
    lang: 'en',
    // Strict loading mode
    strict: 0
  }
}
typescript

Step 4: Add type definitions#

Since I’m using TypeScript, I need proper types for the Giscus config. Here’s the schema I added to my integrations-config.ts:

/** The Giscus comment system */
giscus: z.object({
  /** Enable the Giscus comment system. */
  enable: z.boolean().default(false),
  /** GitHub repository for comments (format: owner/repo). */
  repo: z.string(),
  /** Repository ID. */
  repoId: z.string(),
  /** Discussion category. */
  category: z.string().default('Comments'),
  /** Category ID. */
  categoryId: z.string(),
  /** Mapping between the page and the discussion. */
  mapping: z.enum(['pathname', 'url', 'title']).default('pathname'),
  /** Enable reactions. */
  reactionsEnabled: z.boolean().default(true),
  /** Emit metadata. */
  emitMetadata: z.boolean().default(false),
  /** Input position. */
  inputPosition: z.enum(['top', 'bottom']).default('bottom'),
  /** Theme. */
  theme: z.string().default('preferred_color_scheme'),
  /** Language. */
  lang: z.string().default('en'),
  /** Strict loading mode (0 = load even if discussion not found, 1 = strict). */
  strict: z.number().default(0)
})
typescript

This gives me autocomplete and type checking for all your Giscus options.

Step 5: Integrate with my blog layout#

The final step is adding the Comment component to my blog post layout. In my BlogPost.astro layout, I import and use the component:

---
import { Comment } from '@/components/giscus'
// ... other imports ...

const {
  post: { data },
  // ... other props ...
} = Astro.props

const { draft: isDraft, comment: enableComment } = data
---

<PageLayout ...>
  <!-- Your post content here -->
  
  <Fragment slot='bottom'>
    {/* Other bottom content like copyright, related posts */}
    
    {/* Comment section - only shows on published posts with comments enabled */}
    {!isDraft && enableComment && <Comment class='mt-3 sm:mt-6' />}
  </Fragment>
</PageLayout>
astro

Notice the conditional: comments only appear on published posts (!isDraft) and when the individual post has comments enabled (enableComment). This gives me granular control - I can disable comments on specific posts by setting comment: false in the frontmatter.

Step 6: Remove Waline#

With Giscus working, it was time to say goodbye to Waline:

  1. Remove the Waline components - Delete Waline component files
  2. Remove Waline dependenciesbun remove @waline/client
  3. Clean up config - Remove Waline-related configuration
  4. Shut down the backend - Decommission the Waline server/function

I don’t believe there’s an automated migration path to Giscus, but I could be wrong. I chose to start fresh since my blog is brand new, but if you have valuable discussions, you might want to try exporting from Waline’s UI to take a look at the output. If you have ever done this type of migration, let me know in the comments.

The Result#

Here’s what I gained from this migration:

AspectBefore (Waline)After (Giscus)
InfrastructureServerless function + DatabaseNone (GitHub handles it)
Monthly cost~$5-10$0
MaintenanceRegular updates neededZero maintenance
AuthCustom or OAuth setupGitHub (readers likely have accounts)
FeaturesRich (reactions, admin panel)Essential (comments, reactions)

The trade-off? Giscus has fewer features than Waline. No admin panel, no anonymous comments, no fancy spam filtering. But for my use case - a tech blog with GitHub-savvy readers - this was a worthwhile trade.

Final Thoughts#

Migrating from Waline to Giscus took me a while to figure out. The actual code changes were minimal - really just creating a new component and updating the configuration.

If you’re running a technical blog and find yourself spending more time maintaining your comment system than writing posts, give Giscus a look.


Have questions about the migration? Drop a comment below - powered by Giscus!

From Waline to Giscus: Migrating My Blog's Comment System
https://pipitonelabs.com/blog/waline-to-giscus
Author Joseph Pipitone
Published at November 25, 2025