Open-source project
CuteLeaf/Firefly avatar
CuteLeaf/Firefly

Firefly: an Astro blog theme built for forking, with an eight-step build

🍀Firefly, fresh and aesthetic Astro blog theme template.

2,261 stars1,809 forksAstroMIT

At a glance

What is it?
Firefly is a MIT Astro blog template at version 6.16.8, derived from the Fuwari theme and configured through twenty-five files in src/config. The recommended workflow is to fork before cloning, the build runs eight custom TypeScript steps including LQIP generation, font subsetting and Pagefind indexing, and five of its six READMEs are machine-translated by the project's own admission.
Who is it for?
Use Firefly if you want a personal blog with search, dark mode, multiple layouts, Mermaid and PlantUML diagrams and diagrams-free code highlighting, and you are content to own the configuration files rather than click through a settings UI.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 9 days ago.
What is it written in?
Mainly Astro, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 3, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The build is eight custom scripts wrapped around astro build

The build command in package.json is the most revealing line in the repository, and it is a single line.

json
"build": "npx tsx scripts/generate-github-card-data.ts && npx tsx scripts/generate-lqips.ts && npx tsx scripts/generate-vndb-covers.ts && astro build && npx tsx scripts/prune-pio-assets.ts && npx tsx scripts/subset-fonts.ts && npx tsx scripts/minify-inline-scripts.ts && npx tsx scripts/run-pagefind.ts"

Eight steps, seven of them custom TypeScript run through tsx, with astro build in the middle. Nothing here is a stock Astro workflow, and reading the filenames tells you what a static blog template in 2026 is expected to do that a 2020 one was not.

Before the build, three data-gathering steps. generate-github-card-data.ts produces the data behind the GitHub repository cards that the Markdown extensions can embed. generate-lqips.ts generates low-quality image placeholders, which is the technique of shipping a tiny blurred preview and swapping in the full image once it loads, and it is the single most effective thing you can do to a text-and-images blog's perceived performance. generate-vndb-covers.ts fetches cover art from VNDB, the visual novel database, so posts about visual novels get artwork without the author uploading it.

Then astro build, and four post-processing steps that all run on the output. prune-pio-assets.ts removes unused assets for the pio widget, which is a mascot-style interface element with its own configuration file. subset-fonts.ts subsets the fonts, which matters a great deal for a theme with a CJK requirement: shipping a full CJK font is tens of megabytes, and subsetting to the glyphs actually used on the site is the difference between a blog that loads and one that does not. minify-inline-scripts.ts minifies the inline scripts the template emits. And run-pagefind.ts builds the Pagefind search index, which is what makes the advertised client-side full-text search work without a server.

Two observations follow. First, the pipeline is competent and the ordering is correct, because LQIPs and subsetting have to happen against real output and Pagefind has to index the final HTML. Second, the build is no longer one command, and the README's deployment instructions tell hosting platforms to run pnpm run build as the build command with the output directory as dist. So a platform's build timeout is now a constraint on a chain of eight steps, and a Vercel or Netlify plan with a short timeout is a real consideration.

The other scripts in package.json mirror the pipeline as individual entry points. There is lqips, github-cards, new-dynamic, new-d and new-post, so each step can be run on its own while iterating. That is the right factoring and it costs nothing.

pnpm is required, and the version is stated three different ways

This repository has the most careful version pinning of any blog template and then contradicts itself about it in the README, which is worth walking through because each contradiction is small and the combination is confusing.

The manifest is unambiguous. packageManager is [email protected], and engines requires node >=22.23.0. Those two lines are what your tooling reads and they are pinned to patch-level precision.

The README's environment requirements say Node.js ≥ 22 and pnpm ≥ 11. The Node figure is looser than the manifest by twenty-three minor versions, which is the kind of gap that produces an install failure on Node 22.0 that the requirements section said was fine.

The pnpm figure is worse, because there are three values. The requirements text says pnpm ≥ 11. The badge's alt text says pnpm >= 11. And the badge image itself is a shields.io URL whose encoded text is pnpm >= 9, so the badge that renders in the README says nine while the text around it says eleven. One of those is wrong, and the most likely explanation is that the requirements were raised from 9 to 11 and the badge URL was not.

None of this is fatal. packageManager is authoritative and a project using corepack or a pnpm version manager will get 11.22.0 whatever the README says. But a reader who is deciding whether to fork this template is reading the README, and the README gives them two contradictory pnpm floors and an optimistic Node floor.

The enforcement mechanism is worth noting as well, because it is aggressive in a useful way:

json
"preinstall": "npx only-allow pnpm"

A preinstall hook that fails the install unless the package manager is pnpm. So npm install and yarn install do not silently produce a broken node_modules, they stop. That is the right call for a project with a pnpm-lock.yaml, since a lockfile from another package manager produces a tree that differs from what the author tested.

The setup instructions handle the bootstrap: install pnpm globally with npm install -g pnpm, then run pnpm install. Using npm to install pnpm and then refusing npm for everything else is a slightly awkward dance, and it is why that step is written out explicitly rather than left to a package-manager section.

The type checking is strict in a way worth mentioning too. type-check runs tsc --noEmit --isolatedDeclarations, and isolatedDeclarations requires every exported value to carry an explicit type annotation. For a theme whose config files are consumed by other code, that flag is doing real work: it guarantees the published types are self-contained rather than depending on inference that a consumer's TypeScript version might resolve differently.

The recommended workflow is fork, then clone, which makes updates manual

The installation instructions contain a piece of advice that tells you more about the project's maintenance model than any configuration file does.

The README shows the normal clone first, git clone https://github.com/Cuteleaf/Firefly.git, and then immediately follows it with a bolded note and a second, recommended alternative: fork the repository to your own account first, then clone that, and remember to star before forking.

So the intended workflow is star, fork, clone your fork. Not clone upstream and remap later. Fork first, and your copy diverges from upstream on the first commit you make.

That advice is not arbitrary. A blog is a repository you own, with your posts, your configuration and your own domain configuration. Forking is the natural way to get your own copy, and the alternative, cloning upstream and pushing to your own remote, is more steps and produces the same divergence. The author is optimising for the common case.

The consequence is the part that is not stated. Because your copy is a fork, every upstream commit reaches you as a merge. And there is no version to fall back to, because the repository has no GitHub releases. The version lives in package.json, which reads 6.16.8, and the deployment buttons deploy from the repository, so a blog is built from whatever the merge produced rather than from a tagged artefact.

At version 6.16.8 with a last push on 2026-09-24, the upstream is active. But the shape is: a theme with sixteen minor versions, a fork-first workflow and no releases, where keeping current means merging a theme you have heavily customised, in a repository that also contains your own writing.

For most personal blogs that is a non-event, because a blog gets set up once and left alone. For anyone who wants to adopt new theme features later, the cost is a merge across a file tree where the author has edited twenty-five configuration files and the user has edited the same twenty-five configuration files. Configuration conflicts in a fork are the predictable outcome.

The mitigation is the structure the project already provides. Because nearly all user-facing settings live in src/config rather than in components, a merge that conflicts in those files is a merge you can resolve by choosing your side, and the component code underneath comes through clean. That is a good reason for the config-file approach, arrived at for a different reason than it was probably designed for.

Five of six READMEs are machine-translated, and Russian is missing entirely

The README states its own translation quality, which is rare and directly relevant to anyone reading it in English.

Firefly supports i18n for the interface, with the UI supporting Simplified Chinese, Traditional Chinese, English, Japanese, Russian and Korean. But the note in the tip block says that except for Simplified Chinese, all other languages are AI-translated conversions, and invites pull requests to fix any errors.

So the English README you are reading is a machine translation of the Simplified Chinese original, and the project says so. The same applies to the Traditional Chinese, Japanese, Russian and Korean versions.

That is a better position than the alternative. A theme with a Chinese-speaking author and a user base spanning several languages has three honest options: ship one language, ship machine translations and label them, or ship nothing and let people translate it themselves. This project chose the second and labelled it, which means nobody is misled about what they are looking at and the pull request invitation has a clear purpose.

The five README links at the top of the file are the corresponding set: 简体中文 pointing at README.md, 繁體中文 at docs/README.zh-TW.md, English at README.en.md, 日本語 at docs/README.ja.md, and 한국어 at docs/README.ko.md.

Which is four translated versions plus the original, against six supported interface languages. Russian is in the supported language code list and in the feature list, and there is no Russian README. So the interface ships in Russian and the documentation does not.

The supported language codes are listed explicitly, which is the thing a user actually needs:

typescript
// 定义站点语言
const SITE_LANG = "zh_CN";

with zh_CN for Simplified Chinese, zh_TW for Traditional Chinese, en, ja, ru and ko. Six codes, set in src/config/siteConfig.ts.

The per-post lang field is worth understanding alongside that. The frontmatter schema carries a lang key with a comment saying it is only needed when the post's language differs from the site language in siteConfig.ts. So the default is site-wide and the frontmatter is the per-post override, which is the right design for a blog that is mostly one language with occasional posts in another.

A reader assessing this project in English should weigh the machine translation accordingly. The technical content, the file names and the config keys are all correct because they are identifiers. The prose is a faithful rendering of what the author wrote in Chinese, which means the author's opinions and caveats come through, and the things most likely to be lost are exactly the ones that are already marginal: the tone of a recommendation, the emphasis on a caveat, the difference between a requirement and a suggestion. None of that is load-bearing for a configuration reference.

Twenty-five config files, one of them named differently from the rest

The configuration model is the design decision that makes this theme flexible, and it is unusual enough to be worth describing precisely.

Almost everything a user would want to change lives in files under src/config rather than in a settings panel in the browser. The README's tree listing shows twenty-five entries in that directory: an index.ts, then siteConfig.ts, analyticsConfig.ts, announcementConfig.ts, backgroundWallpaper.ts, commentConfig.ts, coverImageConfig.ts, displaySettingsConfig.ts, dynamicConfig.ts, effectsConfig.ts for animation effects such as falling petals, expressiveCodeConfig.ts, fontConfig.ts, friendsConfig.ts, galleryConfig.ts, licenseConfig.ts, musicConfig.ts, navBarConfig.ts, pioConfig.ts, mermaidConfig.ts, plantumlConfig.ts, profileConfig.ts, sidebarConfig.ts and sponsorConfig.ts.

That is twenty-four TypeScript modules plus FooterConfig.html, which is both a different extension and a different naming convention from every other file in the directory.

The naming inconsistency is cosmetic and worth noting only because it is the kind of thing that makes a config directory feel hand-assembled rather than designed. Everything else is camelCase with a Config suffix except index, backgroundWallpaper and profileConfig, and one file is PascalCase with an .html extension. If you are writing a script that iterates the config directory, or you are grepping for config files, that one file will not match the pattern.

The content of the directory is the real story. It covers analytics, which is where you would put a privacy-respecting analytics endpoint. It covers comments, so the comment system is a choice rather than a hard dependency. It covers the background wallpaper separately from effects, which is why the theme can offer the four wallpaper modes the README demonstrates: banner, fullscreen wallpaper, fullscreen transparent wallpaper, and solid colour, with a separate dynamic overlay option. It covers the font selector, which is why custom fonts are a supported feature rather than a hack. It covers a display settings panel, which is where the sidebar, list layout and grid layout options live.

And it has three separate configuration files for rendering content, expressiveCodeConfig for code blocks, mermaidConfig for Mermaid diagrams and plantumlConfig for PlantUML. That is a theme that treats diagrams and syntax highlighting as first-class rather than as a plugin you add, and the Markdown extensions section confirms it with admonitions in four themes, GitHub repository cards and Expressive Code enhanced code blocks.

There is also a music player config, a friend-links config, a gallery config, a licence config for your content, a sponsor config, a profile config, a nav bar config, a cover image config, an announcement config and a pio widget config. The pio widget is the one most blog templates do not have.

The cost of this approach is that there is no UI for any of it. Every one of those settings is a file edit, a pull request, or a commit from a text editor. The benefit is that every one of them is greppable, diffable, reviewable and version-controlled, and none of them is stored in a database or a localStorage blob that a theme update can clobber. For a repository you fork and own, the file-based model is the right trade, and it is the same property that makes theme upgrades survivable.

Fuwari is the other upstream, and both layouts are still in the config

The most structurally important sentence about this theme is in the tip block, and it names a second project.

Firefly is described as a fresh and attractive personal blog theme template built on the Astro framework and the Fuwari template, designed for technology enthusiasts and content creators. And then: Firefly also retains the original Fuwari layout, which you can switch between freely in the configuration file.

So this is not a theme built from nothing. It is a substantial rework of Fuwari, another Astro blog theme, and the original layout is still present in the codebase as a selectable option. There is a documentation page dedicated to the layout system, linked from the README as Firefly Layout System Details.

That is a meaningful design decision. Most derivative themes replace the thing they were derived from. This one keeps it, which means the repository carries two complete visual designs and a config switch between them, and users who preferred Fuwari's original look can stay on it while using the newer feature set.

It also means there are two upstream dependencies rather than one. Astro is the framework and it is a real dependency with a major version boundary. Fuwari is a design dependency with no version boundary at all, because a fork of a design is not tracked as a package. When Fuwari changes, Firefly does not get it. When Firefly changes, Fuwari does not get it. The retained layout is a copy, and copies drift.

The version number is the other signal. package.json reads 6.16.8, which is a long way past a first release for a blog theme, and there are no GitHub releases to match those versions against. So the six major lines are implicit in the manifest rather than declared in tags, and anyone who forked at an earlier version has no tag to compare against.

For a technical writer, the fork-first workflow and the configuration-file approach interact well here. The twenty-five config files are where a user spends their time, and they are structured so that a merge from upstream conflicts in the files you customised and passes cleanly through the ones you did not. The retained Fuwari layout is the opposite case, since it is component code, so a merge there is a merge you have to read.

The project is also honest about the trade it has made. The attribution note asks that if you reference or use Firefly's component designs and related code, please credit Firefly, and that request goes beyond what the MIT licence in the repository requires, which is to retain the copyright notice. Asking for design credit in addition to code attribution is a reasonable request from a designer and it is worth a reader knowing about, because it signals that the design work is the part the author considers worth naming.

Two deployment targets are pre-configured, and the third is a button

The deployment story is thorough in a way that is easy to miss under the feature list, and it tells you which platforms the author actually uses.

The repository ships vercel.json and wrangler.jsonc at the root. Those are the two platforms with a configuration file committed: Vercel, and Cloudflare through wrangler, which is Wrangler's JSON-with-comments format and therefore the Cloudflare Workers and Pages toolchain. Netlify has no config file, and neither does EdgeOne Pages, which the README also lists as a target.

The README's own platform-hosting section points at Astro's official deployment guide and names Vercel, Netlify, Cloudflare Pages and EdgeOne Pages as the destinations. It then states that mainstream platforms such as Vercel and Netlify deploy automatically and will select the appropriate adapter based on the environment. The build settings it gives are the framework preset Astro, the root directory ./, the output directory dist, the build command pnpm run build and the install command pnpm install.

That is a complete, copyable configuration. The one thing to notice is that the build command is the eight-step chain, not astro build, so the build timeout consideration from earlier applies to every one of these platforms equally.

The dependency list backs up the Cloudflare claim. Alongside the Astro integrations for markdown, MDX, RSS, sitemap, Svelte and check, there is @astrojs/cloudflare as a direct dependency, which is an adapter package that only makes sense if Cloudflare is a first-class target rather than an afterthought. The README's claim that the adapter is chosen automatically per environment is exactly what that adapter is for.

There are also deploy buttons for Vercel and Netlify at the bottom of the section, both pointing at the upstream repository rather than at a template, so a new user forks first and then deploys from their own copy. That is consistent with the fork-first advice and it is the right default, because a deploy button pointed at upstream would deploy a site with the author's configuration.

The other deployment-relevant file is pagefind.yml, which configures the Pagefind search indexer, plus public/ for static assets and src/content/dynamic/ for the status-update feature. The dynamic feature is worth one more note because it is the least blog-template thing in the theme: one Markdown file per entry, a pnpm new-d shortcut to create one, frontmatter with published, pinned and location fields, and an option in dynamicConfig.ts to pull entries live from a Memos instance instead, with pinned-state synchronisation and image attachments. That is a microblog wired into a blog, sourced from a self-hosted note system, and it is the kind of feature that makes a template feel like it grew out of one person's real site.

Editorial conclusion

Use Firefly if you want a personal blog with search, dark mode, multiple layouts, Mermaid and PlantUML diagrams and diagrams-free code highlighting, and you are content to own the configuration files rather than click through a settings UI. Do not use it if you need a theme that stays in sync with upstream, because the recommended workflow is to star, fork and clone your own copy, so every future upstream commit reaches you as a manual merge, and there are no GitHub releases to fall back to. Do not rely on the English README as a precise specification, since the project states that every language except Simplified Chinese is AI-translated. Verify five things. Confirm your Node is at least 22.23.0 rather than the 22 the badge advertises, and install pnpm 11.22.0, because the manifest pins both and a preinstall hook rejects npm and yarn outright. Read the frontmatter schema before writing posts, since lang, pinned, comment and draft all have specific meanings and the image field doubles as a random-cover switch. Plan for the build time, because the build command a deploy platform runs is a chain of eight steps including font subsetting and a Pagefind index, not a single astro build. And read the Fuwari relationship before you customise the layout, because the original Fuwari layout is still selectable in the config and the two designs have diverged. The deciding fact is that this is a template you take ownership of, and the fork-first advice is the author telling you so explicitly.

Frequently asked questions

How do I set up a Firefly blog?

The recommended path is to star the repository, fork it to your own account, then clone your fork and run pnpm install. Requirements are Node.js and pnpm, and the manifest pins node >=22.23.0 and packageManager [email protected], with a preinstall hook using only-allow pnpm that refuses npm and yarn. Then edit the files in src/config/ and run pnpm dev, which serves the blog at http://localhost:4321.

What does the Firefly build actually do?

The build command is a chain of eight steps: generate GitHub card data, generate LQIP image placeholders, fetch VNDB cover art, run astro build, prune pio assets, subset fonts, minify inline scripts, and run the Pagefind search indexer. Seven of the eight are custom TypeScript scripts run through tsx, and hosting platforms are told to use pnpm run build with the output directory as dist.

How do I configure a blog post in Firefly?

Posts use YAML frontmatter with title, published, description, image, tags, category, draft, lang, pinned and comment. The image field also accepts the value api to enable a random cover image. The lang key is only needed when a post's language differs from the site language set in src/config/siteConfig.ts.

Which languages does Firefly support?

The interface supports zh_CN, zh_TW, en, ja, ru and ko, set through the SITE_LANG constant in src/config/siteConfig.ts. The README states that except for Simplified Chinese every other language is an AI translation and invites pull requests to correct errors, and there is no Russian README even though Russian is a supported interface language.

How do I keep a forked Firefly blog up to date?

By merging. The recommended workflow is to fork the repository and clone your own copy, so upstream commits arrive as merges rather than as an upgrade, and the repository has no GitHub releases to fall back on even though package.json carries version 6.16.8. Merges usually conflict in src/config, where your settings live, rather than in the component code underneath.

Can I use the Fuwari layout in Firefly?

Yes. Firefly was developed on the Astro framework and the Fuwari template, and it retains the original Fuwari layout, which you can switch between in the configuration file. The layout system is documented on a page linked from the README as Firefly Layout System Details, alongside the newer layouts covering banner, fullscreen wallpaper, fullscreen transparent wallpaper and solid colour.

Official sources

  1. CuteLeaf/Firefly on GitHub
  2. Issues
  3. License: MIT
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/cuteleaf-firefly.svg)](https://hysenlabs.com/projects/cuteleaf-firefly)