Back to blog

Migrate from Giscus to a hosted comment system without losing threads

Giscus keeps every comment in a GitHub Discussion, which is exactly why leaving it is easy: the data is yours and the API is public. This is the step-by-step export, the conversion to a JSON import, and the snippet swap that keeps each discussion attached to the page it belongs to.

Giscus is a good piece of software. It is free, it has no server of its own, and it stores every comment in a GitHub Discussion in a repository you control. People leave it for one reason that has nothing to do with quality: to comment, a reader has to have a GitHub account and authorise the giscus app, and most readers of most blogs do not have one. If you have already decided to move, this post is the how. If you are still deciding, the best Giscus alternative post covers the which. This one assumes you are moving to EchoThread; the export steps work for any destination that can read a JSON file.

We build EchoThread, so treat our claims as claims. The GitHub parts of this guide are GitHub's own API, and you can check them against the GraphQL documentation.

What you are actually moving

Giscus maps each page to one discussion in one category of your repository. The mapping is whatever your data-mapping attribute says: by default the discussion title contains the page's pathname; the alternatives are the full URL, the </code> tag, the <code>og:title</code> meta tag, a specific term you chose, or a fixed discussion number (from <a href="https://giscus.app/" target="_blank" rel="noopener">giscus.app</a>, read on 3 September 2026). Top-level comments are discussion comments; replies are discussion replies, one level deep; reactions live on the discussion and on each comment.</p><p>Three things follow. The mapping tells you how to reattach each thread to its page on the new system. Replies survive, because the import format has a parent field. Reactions do not: EchoThread's import has no field for them, so they stay in GitHub. Which is the fourth point: do not delete the discussions. They are your archive and your rollback.</p><h2>Step 1: read your own snippet</h2><p>Open the page template that carries the giscus script and note three attributes:</p><pre><code class="language-html"><script src="https://giscus.app/client.js" data-repo="yourname/yourrepo" data-category-id="DIC_kwDOxxxxxxxxxxxx" data-mapping="pathname" ...></script></code></pre><p><code>data-repo</code> and <code>data-category-id</code> are the two identifiers the export query needs, and <code>data-mapping</code> tells you what the discussion titles contain. If it is <code>pathname</code>, the title is the path (<code>/blog/my-post/</code>). If it is <code>url</code>, the title is the full URL. If it is <code>title</code> or <code>og:title</code>, you will need to map titles back to URLs yourself in step 3.</p><h2>Step 2: export the discussions with the GraphQL API</h2><p>The <a href="https://cli.github.com/" target="_blank" rel="noopener">gh CLI</a> can run GraphQL queries and paginate them. Save this as <code>discussions.graphql</code>:</p><pre><code class="language-graphql">query($owner: String!, $name: String!, $category: ID!, $endCursor: String) { repository(owner: $owner, name: $name) { discussions(first: 50, after: $endCursor, categoryId: $category) { pageInfo { hasNextPage endCursor } nodes { number title url createdAt comments(first: 100) { totalCount nodes { id bodyHTML createdAt author { login } replies(first: 100) { nodes { id bodyHTML createdAt author { login } } } } } } } } }</code></pre><p>Then run it with the values from your snippet:</p><pre><code class="language-bash">gh api graphql --paginate --slurp \ -F owner=yourname -F name=yourrepo \ -F category=DIC_kwDOxxxxxxxxxxxx \ -F query=@discussions.graphql > discussions.json</code></pre><p><code>--paginate</code> follows <code>endCursor</code> until every discussion is fetched, and <code>--slurp</code> writes the pages as one JSON array. The query asks for <code>bodyHTML</code> rather than <code>body</code> because the import accepts HTML and GitHub has already rendered the Markdown, mentions and code blocks for you. <code>author</code> is <code>null</code> for a deleted account; the conversion below turns that into "Anonymous". If a single discussion has more than 100 comments, or a comment has more than 100 replies, check <code>totalCount</code> and page that discussion separately. Most blogs never hit it.</p><h2>Step 3: convert to the JSON import shape</h2><p>EchoThread's <a href="https://echothread.io/docs/#import-json">JSON import</a> takes a list of threads. Each thread has a <code>url</code>, an optional <code>title</code> and <code>identifier</code>, and a list of comments with <code>id</code>, <code>author</code>, <code>body</code>, <code>created_at</code>, <code>parent_id</code> and <code>status</code>. The only required fields are the thread <code>url</code> and each comment's <code>body</code>; <code>status</code> defaults to approved, and <code>parent_id</code> is how a reply points at its parent.</p><p>This script does the conversion for the default <code>pathname</code> mapping. Set <code>SITE</code> to the origin your pages live on.</p><pre><code class="language-python">import json SITE = "https://example.com" # no trailing slash pages = json.load(open("discussions.json")) threads = [] for page in pages: for d in page["data"]["repository"]["discussions"]["nodes"]: path = d["title"].strip() # pathname mapping: the title is the path comments = [] for c in d["comments"]["nodes"]: author = (c["author"] or {}).get("login") or "Anonymous" comments.append({"id": c["id"], "author": author, "body": c["bodyHTML"], "created_at": c["createdAt"], "parent_id": None}) for r in c["replies"]["nodes"]: rauthor = (r["author"] or {}).get("login") or "Anonymous" comments.append({"id": r["id"], "author": rauthor, "body": r["bodyHTML"], "created_at": r["createdAt"], "parent_id": c["id"]}) if comments: threads.append({"url": SITE + path, "title": path, "identifier": path, "comments": comments}) json.dump({"threads": threads}, open("echothread-import.json", "w"), indent=1) print(len(threads), "threads,", sum(len(t["comments"]) for t in threads), "comments")</code></pre><p>Two decisions are baked in. <code>identifier</code> is set to the path, which is what you will put in the new snippet's <code>data-identifier</code>, so the imported thread and the live page line up even if you later change domains. And discussions with zero comments are skipped, because a thread with nothing in it is only noise in the moderation queue.</p><p>If your mapping was <code>url</code>, replace <code>SITE + path</code> with <code>d["title"]</code>. If it was <code>title</code> or <code>og:title</code>, build a dictionary from your site's page titles to their URLs first; your static site generator can emit one in a few lines, and it is worth doing once rather than matching by hand.</p><p>Check the file size. The import accepts up to 10 MB per upload; a blog with a few thousand comments is well under that, and a larger one can be split by thread.</p><h2>Step 4: import</h2><ol><li>Create the site in the <a href="https://echothread.io/dashboard">dashboard</a> with the domain you publish to, and copy its API key.</li><li>Open the site's Import page, choose the JSON file, and run it. The import runs in the background and shows progress per thread.</li><li>Check the result: threads created, comments created, and any rows it skipped with the reason.</li></ol><p>Imported comments appear as guest comments carrying the original author's display name, with their original dates, threaded under their parents. Deduplication is by thread identifier, so a second run of the same file adds the comments again rather than replacing them; import each file once.</p><h2>Step 5: swap the snippet</h2><p>Replace the giscus script with the EchoThread div on the same template, and set <code>data-identifier</code> to the same value the import used, the page's path:</p><pre><code class="language-html"><div id="echothread" data-api-key="YOUR_API_KEY" data-page-url="https://example.com/blog/my-post/" data-identifier="/blog/my-post/" data-page-title="My post"></div> <script src="https://cdn.echothread.io/widget.js" async></script></code></pre><p>In a static site generator each attribute is a template variable. There are worked versions for <a href="https://echothread.io/docs/guides/hugo/">Hugo</a>, <a href="https://echothread.io/docs/guides/jekyll/">Jekyll</a>, <a href="https://echothread.io/docs/guides/astro/">Astro</a>, <a href="https://echothread.io/docs/guides/eleventy/">Eleventy</a>, <a href="https://echothread.io/docs/guides/nextjs/">Next.js</a> and <a href="https://echothread.io/docs/guides/gatsby/">Gatsby</a>, and a post for <a href="https://echothread.io/blog/add-comments-to-docusaurus/">Docusaurus</a>, which is where a lot of giscus installs live. Deploy, open a page that had comments, and the imported thread should be under it.</p><p>Leave the giscus category in place. It costs nothing, it holds the reactions the import could not carry, and if anything went wrong you can re-run the export.</p><h2>What changes for your readers</h2><p>The reason you moved: a reader no longer needs a GitHub account. On EchoThread they can comment as a guest if you allow it, or sign in with Google, GitHub, X or Facebook. Guest comments are scored by a machine-learning spam classifier before they are published, and the uncertain ones go to your moderation queue, which is the other thing giscus never had: GitHub's abuse controls protect GitHub, not your comment section. The <a href="https://echothread.io/blog/comment-system-migration-checklist-2026/">migration checklist</a> covers the rest of the move: redirects, the canonical URL, and telling readers.</p><p>The free plan is one site, comments included, no ads, no reader tracking. A site created from 1 October 2026 includes 10,000 page views a month on the free plan; a site created before that date keeps unmetered page views. Paid plans start at $9 a month for three sites; the <a href="https://echothread.io/pricing/">pricing page</a> has the ladder. And the move is reversible: every comment can be exported again as JSON or CSV at any time, so you are never more locked in than you were with a GitHub repository.</p> </div> </article> <nav class="article-nav" aria-label="Article navigation"> <a href="https://echothread.io/blog/free-comment-system-no-ads-no-tracking"> <span class="article-nav-label"><span class="arrow">←</span> Previous</span> <span class="article-nav-title">Free comment system with no ads and no tracking: what 'free' actually includes in 2026</span> </a> <a href="https://echothread.io/blog/migrate-from-utterances" class="next"> <span class="article-nav-label">Next <span class="arrow">→</span></span> <span class="article-nav-title">Migrate from Utterances to hosted comments</span> </a> </nav> <section class="comments" aria-labelledby="comments-heading"> <p class="comments-eyebrow">Discussion</p> <h2 id="comments-heading">Comments</h2> <p class="comments-note">This thread runs on EchoThread — the same widget you would <a href="https://echothread.io/register?utm_source=blog&utm_medium=comments&utm_campaign=migrate-from-giscus">add to your own site</a>.</p> <div id="echothread" data-api-key="HZZ3vhpZNYv25NMmEHHUeAutv3t69Q-oHnFcb2a0Pm4" data-page-url="https://echothread.io/blog/migrate-from-giscus" data-identifier="migrate-from-giscus" data-page-title="Migrate from Giscus to a hosted comment system without losing threads" data-theme="light" data-accent-color="#d6353b"></div> <script src="https://cdn.echothread.io/widget.js" async></script> </section> <aside class="sources" aria-labelledby="sources-heading"> <h2 id="sources-heading">Sources and standards</h2> <p>Claims about performance, cookies, consent, and search-visible page experience are checked against these primary references:</p> <ul> <li><a href="https://web.dev/articles/vitals" target="_blank" rel="noopener">Google Core Web Vitals</a></li> <li><a href="https://httparchive.org/reports/page-weight" target="_blank" rel="noopener">HTTP Archive Page Weight</a></li> <li><a href="https://eur-lex.europa.eu/eli/dir/2002/58/art_5/par_3/oj/eng" target="_blank" rel="noopener">EU ePrivacy Directive Article 5(3)</a></li> <li><a href="https://eur-lex.europa.eu/eli/reg/2016/679/art_7/oj/eng" target="_blank" rel="noopener">GDPR Article 7 consent conditions</a></li> </ul> </aside> <aside class="related" aria-labelledby="related-heading"> <p class="related-eyebrow">Related guides</p> <h2 id="related-heading">Keep reading</h2> <div class="related-grid"> <a href="https://echothread.io/blog/migrate-from-utterances" class="related-card"> <span class="related-card-num">01</span> <div class="related-card-title">Migrate from Utterances to hosted comments</div> <div class="related-card-excerpt">Utterances stores each page's comments as one GitHub Issue, so the export is a few gh commands and the data never leaves your hands. Here is the full move: export, convert, import, swap the snippet, and what your readers get that they did not have.</div> <svg class="related-card-arrow" fill="none" stroke="currentColor" stroke-width="2.25" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"/></svg> </a> <a href="https://echothread.io/blog/cusdis-alternative" class="related-card"> <span class="related-card-num">02</span> <div class="related-card-title">Cusdis is deprecated: how to move your comments to a hosted alternative</div> <div class="related-card-excerpt">Cusdis, the 5 KB open-source Disqus alternative, is deprecated: its repository was archived on 17 July 2026 and the README now tells users to email for a data export. This is what Cusdis got right, what it never had, and how to move an existing comment archive to a hosted replacement without losing it.</div> <svg class="related-card-arrow" fill="none" stroke="currentColor" stroke-width="2.25" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"/></svg> </a> <a href="https://echothread.io/blog/comments-for-vitepress-and-mkdocs" class="related-card"> <span class="related-card-num">03</span> <div class="related-card-title">Comments for VitePress and MkDocs documentation sites without a GitHub login</div> <div class="related-card-excerpt">Documentation sites default to giscus or utterances, and both require every reader to authorise a GitHub app before they can ask a question. This guide adds a hosted comment section that lets readers comment as guests, with the exact theme extension for VitePress and the template override for MkDocs and Material for MkDocs.</div> <svg class="related-card-arrow" fill="none" stroke="currentColor" stroke-width="2.25" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"/></svg> </a> <a href="https://echothread.io/blog/publii-disqus-alternative" class="related-card"> <span class="related-card-num">04</span> <div class="related-card-title">Disqus alternative for Publii: what still works in 2026, and what was removed</div> <div class="related-card-excerpt">In August 2026 Publii reviewed every comment plugin in its marketplace: two were added, three were updated, and three were removed, including Cusdis, the option most forum threads used to recommend. Here is the current list, what each one costs, and how to move off Disqus without losing your threads.</div> <svg class="related-card-arrow" fill="none" stroke="currentColor" stroke-width="2.25" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"/></svg> </a> </div> </aside> <div class="cta"> <h2>Ready to try EchoThread?</h2> <p>Free for your first site. Set up in under a minute.</p> <a href="https://echothread.io/register?utm_source=blog&utm_medium=article&utm_campaign=migrate-from-giscus" class="btn-cta"> Create free account <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"/></svg> </a> </div> </main> <footer class="footer"> <p>© 2026 EchoThread. Privacy-first comments for the modern web.</p> <p class="footer-links"><a href="https://echothread.io/alternatives?utm_source=blog&utm_medium=footer&utm_campaign=blog_footer">Compare comment systems</a> · <a href="https://echothread.io/pricing?utm_source=blog&utm_medium=footer&utm_campaign=blog_footer">Pricing</a> · <a href="https://echothread.io/docs?utm_source=blog&utm_medium=footer&utm_campaign=blog_footer">Docs</a></p> </footer> </body> </html>