Site Configuration
The site configuration is the core configuration file of the Firefly theme, controlling basic site information, theme colors, page toggles and other global settings.
Config File
src/config/siteConfig.ts
Basic Information
| Property | Type | Default | Description |
|---|---|---|---|
title | string | "Firefly" | Site title |
subtitle | string | "Demo site" | Site subtitle |
site_url | string | - | Site URL |
description | string | - | Site description for <meta name="description"> |
keywords | string[] | - | Site keywords for <meta name="keywords"> |
lang | string | "zh_CN" | Site language: zh_CN, zh_TW, en, ja, ru, ko. Overridable via PUBLIC_SITE_LANG (see tip below) |
export const siteConfig: SiteConfig = {
title: "Firefly",
subtitle: "Demo site",
site_url: "https://firefly.cuteleaf.cn",
description: "A beautiful Astro blog theme...",
keywords: ["Firefly", "Astro", "Blog"],
lang: "zh_CN",
};Override site language via environment variable
lang can be overridden by the PUBLIC_SITE_LANG environment variable, which takes priority over the config file. For example, set PUBLIC_SITE_LANG=en on your deployment platform (Vercel / Cloudflare, etc.) to build the whole site (navbar, page text) in English without touching the config. Accepts values like en / zh_cn / zh_tw / ja / ja_jp / ru / ko (case-insensitive); falls back to the config default if unrecognized.
Theme Color
| Property | Type | Default | Description |
|---|---|---|---|
themeColor.hue | number | 165 | Theme color hue (0-360). Red: 0, Cyan: 200, Teal: 250, Pink: 345 |
themeColor.defaultMode | string | "system" | Default mode: "light", "dark", "system" |
Page Width
| Property | Type | Default | Description |
|---|---|---|---|
pageWidth | number | 100 | Maximum page width in rem. Larger values make the content area wider |
// Page width (unit: rem)
// Increase the value to make the content area wider
pageWidth: 100,Card Style
| Property | Type | Default | Description |
|---|---|---|---|
card.border | boolean | true | Enable card border and shadow for a 3D effect |
card.followTheme | boolean | false | Whether card background follows theme hue in light mode |
card: {
border: true,
followTheme: false,
},Navbar
| Property | Type | Default | Description |
|---|---|---|---|
navbar.logo | object | - | Navbar logo, see below |
navbar.title | string | "Firefly" | Navbar title |
navbar.widthFull | boolean | false | Whether navbar takes full width |
navbar.menuAlign | string | "center" | Desktop menu alignment: "left" or "center" |
navbar.followTheme | boolean | false | Whether navbar icon and title follow theme color |
navbar.navbarMode | "static" | "fixed" | "dynamic" | "fixed" | Navbar mode: static scrolls away; fixed stays pinned at the top; dynamic is pinned but hides on scroll down and reveals on a slight scroll up |
Logo supports four types:
- Astro icon library:
{ type: "icon", value: "material-symbols:home-pin-outline" } - public directory image (no optimization):
{ type: "image", value: "/assets/images/logo.webp", alt: "Logo" } - src directory image (auto-optimized, recommended):
{ type: "image", value: "assets/images/logo.webp", alt: "Logo" } - Remote image:
{ type: "url", value: "https://example.com/logo.png", alt: "Logo" }
| Property | Type | Default | Description |
|---|---|---|---|
logo.type | string | - | "icon", "image" or "url" |
logo.value | string | - | Icon name, image path or image URL |
logo.valueDark | string | - | A different image for dark mode. Only applies to image and url types |
logo.alt | string | - | Alt text for the image |
With valueDark set, light and dark mode each show their own image and switching themes takes effect instantly. If omitted, both modes share value. Icon library logos (type: "icon") follow the text color, so no separate dark variant is needed.
navbar: {
logo: {
type: "image",
value: "assets/images/firefly.png",
// Use a different image in dark mode (optional)
valueDark: "assets/images/firefly-dark.png",
alt: "🍀",
},
title: "Firefly",
widthFull: false,
menuAlign: "center",
followTheme: false,
navbarMode: "fixed",
},TIP
The share poster header shows the navbar logo and the site title. Since the poster background is always white, the poster always uses the light-mode logo.
Favicon
favicon: [
{
src: "/favicon/favicon.ico",
// theme: "light", // Optional: 'light' | 'dark'
// sizes: "32x32", // Optional: icon size
},
],Date & Timezone
| Property | Type | Default | Description |
|---|---|---|---|
siteStartDate | string | - | Site start date (YYYY-MM-DD), used for uptime counter |
timezone | string | "Asia/Shanghai" | IANA timezone string for date formatting |
Admonitions
| Property | Type | Default | Description |
|---|---|---|---|
rehypeCallouts.theme | string | "github" | Theme: "github", "obsidian", "vitepress", "docusaurus" |
rehypeCallouts.enablePythonMarkdownAdmonitions | boolean | false | Enable Python Markdown style admonition syntax (using !!! instead of > [!NOTE]) |
TIP
Restart the dev server after changing this setting.
Post Settings
| Property | Type | Default | Description |
|---|---|---|---|
post.showLastModified | boolean | true | Show "last modified" card at the bottom of posts |
post.outdatedThreshold | number | 30 | Days threshold for showing the "last modified" card |
post.share | boolean | true | Show share button on post pages |
post.postNavigation | boolean | true | Show previous/next post navigation |
post.relatedPosts | boolean | true | Show related posts recommendation |
post.randomPosts | boolean | true | Show random posts recommendation |
post.generateOgImages | boolean | false | Generate OpenGraph images (increases build time) |
Post List Layout
| Property | Type | Default | Description |
|---|---|---|---|
postListLayout.defaultMode | string | "list" | Default layout: "list" or "grid" |
postListLayout.mobileDefaultMode | string | - | Mobile default layout: "list" or "grid". If not set, it follows defaultMode |
postListLayout.coverPosition | string | "right" | Cover image side in list mode: "right" or "left". Grid mode always places the cover on top and is unaffected |
postListLayout.descriptionLines | number | 2 | Number of lines for post excerpts. Set to 0 to disable truncation |
postListLayout.showStatsIcons | boolean | true | Show icons in the post card footer stats (published date, word count, reading time) |
postListLayout.tagsPosition | string | "meta" | Tag display position: "meta" shows in the metadata row below the title, "bottom" shows at the card bottom (replaces stats display, only one can be chosen). In "bottom" mode, tags beyond meta.tagCount are collapsed into a +N pill; hover it to see the hidden tags |
postListLayout.tagsBottomStyle | string | "chip" | Bottom tag style, only effective when tagsPosition is "bottom": "chip" filled buttons whose shape follows tagStyle; "text" plain text with no background |
postListLayout.grid.masonry | boolean | false | Enable masonry layout |
postListLayout.grid.columnWidth | number | 320 | Minimum card width in grid mode (px). The browser automatically calculates column count based on container width |
postListLayout.grid.coverFullWidth | boolean | false | Whether the grid-mode cover bleeds to the card edges. true makes the cover reach the top, left and right edges with only the top two corners rounded; false insets the cover by the card padding, leaving a gap on the top, left and right with all four corners rounded (the gap follows the screen width, matching the card padding) |
PostMeta Display Control
Controls the display of each element in the metadata row below the post card title.
| Property | Type | Default | Description |
|---|---|---|---|
postListLayout.meta.showPublished | boolean | true | Show published date |
postListLayout.meta.showCategory | boolean | true | Show category |
postListLayout.meta.showTags | boolean | true | Show tags |
postListLayout.meta.tagCount | number | 1 | Number of tags to display. Set to 0 for no limit. When tagsPosition is "bottom", extra tags are collapsed into a +N pill |
postListLayout.meta.showWords | boolean | true | Show word count |
postListLayout.meta.showReadingTime | boolean | true | Show reading time |
PostStats Display Control
Controls the display of each element in the post card footer stats bar. When tagsPosition is set to "bottom", stats will not be displayed.
| Property | Type | Default | Description |
|---|---|---|---|
postListLayout.stats.showPublished | boolean | true | Show published date |
postListLayout.stats.showWords | boolean | true | Show word count |
postListLayout.stats.showReadingTime | boolean | true | Show reading time |
Category and Tag Styles
Both the category navigation buttons and the tag chips can switch between a "pill" and a "rectangle" shape. The two options are independent of each other.
| Property | Type | Default | Description |
|---|---|---|---|
categoryStyle | string | "rectangle" | Category navigation button style: "pill" or "rectangle" |
tagStyle | string | "pill" | Tag style: "pill" (theme-colored background), "pill-gray" (neutral grey background), or "rectangle" (theme-colored background) |
categoryStyle: "rectangle",
tagStyle: "pill",categoryStyle
Shape only — no color change. "rectangle" keeps every color from the pill variant (including hover, active-category highlight and the post-count badge) and merely reduces the corner radius.
It applies to the category navigation bar only (top of the homepage and archive page, toggled by categoryBar). The filter buttons on the friends, gallery and bookmark pages are unaffected and always stay pills.
tagStyle
| Value | Appearance |
|---|---|
"pill" | Fully rounded pill on a theme-colored background (light tint of --btn-content), deepening to the theme accent on hover |
"pill-gray" | Fully rounded pill on a neutral grey background, turning theme-colored on hover |
"rectangle" | Slightly rounded rectangle using the theme color (--btn-regular-bg) directly, with hover / active shade changes |
It applies to all three tag locations:
- Post card tags (in the metadata row, and the bottom tags including the
+Ncollapse pill whentagsPositionis"bottom") - Tags on the
/tagspage and their post-count badges - The sidebar tags widget
TIP
When postListLayout.tagsBottomStyle is "text" the bottom tags have no background, so tagStyle has no effect there. The other two locations still follow tagStyle.
Pagination
| Property | Type | Default | Description |
|---|---|---|---|
pagination.postsPerPage | number | 10 | Posts per page |
Page Toggles
| Property | Type | Default | Description |
|---|---|---|---|
pages.friends | boolean | true | Friends page toggle |
pages.sponsor | boolean | true | Sponsor page toggle |
pages.guestbook | boolean | true | Guestbook page toggle (requires comment system) |
pages.bangumi | boolean | true | Bangumi page toggle |
pages.vndb | boolean | true | VNDB page toggle |
pages.gallery | boolean | true | Gallery page toggle |
pages.bilibili | boolean | true | Bilibili page toggle |
pages.dynamic | boolean | true | Moments page toggle, including its navigation link and sidebar widget |
pages.booknav | boolean | true | Booknav page toggle |
pages.mal | boolean | true | MyAnimeList page toggle |
Environment variable override
Every page toggle can be overridden by an environment variable, which takes priority over the config file. For example, set PUBLIC_PAGES_BILIBILI=true on your deployment platform (Vercel / Cloudflare, etc.) to enable the Bilibili page without touching the config file, or PUBLIC_PAGES_FRIENDS=false to disable the friends page. Values like true / 1 / on enable, false / 0 / off disable.
| categoryBar | boolean | true | Category navigation bar on homepage and archive page |
Display Settings Panel
The display settings panel is opened via the gear icon in the navbar, allowing visitors to customize theme color, wallpaper mode, card style, and more.
See Display Settings Panel for details.
Bangumi
| Property | Type | Default | Description |
|---|---|---|---|
bangumi.userId | string | - | Bangumi user ID |
bangumi.mode | "static" | "dynamic" | "dynamic" | Data mode. static fetches data at build time and renders statically; dynamic fetches data in the browser via API, always showing the latest data |
bangumi.apiUrl | string | "https://api.bangumi.one" | Bangumi API URL |
bangumi.subjectBaseUrl | string | "https://bangumi.one/subject/" | Subject detail page URL |
bangumi.categoryOrder | string[] | ["anime", "book", "music", "game"] | Category display order. Available values: "anime" "book" "music" "game" "real" |
bangumi.nsfw | "off" | "blur" | "hide" | "off" | NSFW handling: off show all, blur blur covers, hide remove entries |
TIP
In static mode, dev only fetches one page of data; build fetches all data. In dynamic mode, data is fetched in real-time in the browser and is always up-to-date.
VNDB
| Property | Type | Default | Description |
|---|---|---|---|
vndb.userId | string | "" | VNDB user ID, e.g. u2 |
vndb.mode | "static" | "dynamic" | "static" | Data mode. static fetches data at build time and renders statically; dynamic fetches data in the browser via API, always showing the latest data |
vndb.downloadCovers | boolean | false | Download and compress covers into public/vndb-covers at build time so images are served from your own server. static mode only |
vndb.apiUrl | string | "https://api.vndb.org/kana" | VNDB API URL |
vndb.vnBaseUrl | string | "https://vndb.org/" | Entry detail page URL, must end with / |
vndb.apiToken | string | "" | Access token for private lists, static mode only. Never commit a real token to a public repository |
vndb.nsfw | "off" | "blur" | "hide" | "blur" | NSFW handling: off show all, blur blur covers, hide remove entries |
See VNDB for details.
Analytics
| Property | Type | Default | Description |
|---|---|---|---|
analytics.googleAnalyticsId | string | "" | Google Analytics ID |
analytics.microsoftClarityId | string | "" | Microsoft Clarity ID |
analytics.umamiAnalytics.websiteId | string | "" | Umami website ID |
analytics.umamiAnalytics.scriptUrl | string | "https://cloud.umami.is/script.js" | Umami tracking script URL (supports self-hosted Umami) |
analytics.umamiAnalytics.replaysScriptUrl | string | "https://cloud.umami.is/recorder.js" | Umami session replay script URL (supports self-hosted Umami) |
analytics.umamiAnalytics.trackOutboundLinks | boolean | true | Automatically add Umami outbound click events to external links |
analytics.umamiAnalytics.collectWebVitals | boolean | false | Enable data-performance="true" to collect Core Web Vitals |
analytics.umamiAnalytics.replays.enabled | boolean | false | Enable Umami session replay |
analytics.umamiAnalytics.replays.sampleRate | number | 0.15 | Replay sampling rate from 0 to 1; for example, 0.15 records 15% of sessions |
analytics.umamiAnalytics.replays.maskLevel | "moderate" | "strict" | "moderate" | Privacy masking level; moderate masks inputs, strict also masks all page text |
analytics.umamiAnalytics.replays.maxDuration | number | 300000 | Maximum recording length in milliseconds, default 5 minutes |
analytics.umamiAnalytics.replays.blockSelector | string | "" | CSS selector for elements to fully exclude from recording; omitted when empty |
analytics.la51Analytics.Id | string | "" | 51la analytics ID |
analytics.la51Analytics.sdkUrl | string | "" | Custom SDK URL (leave empty to use default) |
analytics.la51Analytics.ck | string | "" | Data separation identifier for multiple statistics IDs |
analytics.la51Analytics.autoTrack | boolean | false | Enable event analysis |
analytics.la51Analytics.hashMode | boolean | false | Enable hash route mode |
analytics.la51Analytics.screenRecord | boolean | true | Enable session recording |
analytics: {
googleAnalyticsId: "",
microsoftClarityId: "",
umamiAnalytics: {
websiteId: "",
scriptUrl: "https://cloud.umami.is/script.js",
replaysScriptUrl: "https://cloud.umami.is/recorder.js",
trackOutboundLinks: true,
collectWebVitals: false,
replays: {
enabled: false,
sampleRate: 0.15,
maskLevel: "moderate",
maxDuration: 300000,
blockSelector: "",
},
},
la51Analytics: {
Id: "",
sdkUrl: "",
ck: "",
autoTrack: false,
hashMode: false,
screenRecord: true,
},
},If you use a self-hosted Umami instance, set analytics.umamiAnalytics.scriptUrl and analytics.umamiAnalytics.replaysScriptUrl to your own script endpoints.
Image Optimization
| Property | Type | Default | Description |
|---|---|---|---|
imageOptimization.formats | string | "webp" | Output format: "avif", "webp", "both" (recommended) |
imageOptimization.quality | number | 85 | Compression quality (1-100), recommended 70-85 |
imageOptimization.noReferrerDomains | string[] | [] | Domains requiring anti-hotlinking handling, supports wildcard * |
WARNING
Astro can only optimize images in the src directory. More images means longer build times.
Anti-Hotlinking (Referrer Policy)
Some image hosts or CDNs (e.g. Bilibili CDN) enforce hotlink protection by checking the Referer request header, causing 403 errors when their images are embedded in your blog.
By configuring noReferrerDomains, Firefly will automatically add a referrerpolicy="no-referrer" attribute to <img> tags matching the specified domains, preventing the browser from sending the Referer header and bypassing hotlink protection.
imageOptimization: {
formats: "webp",
quality: 85,
noReferrerDomains: [
"i0.hdslb.com", // Bilibili CDN
"i1.hdslb.com",
"i2.hdslb.com",
"*.bilibili.com", // Wildcard support
],
},TIP
- Only applies to external images starting with
http://orhttps://, local images are not affected - Only affects
<img>tags with matching domains, does not change referrer behavior for other links - Images with alt text in Markdown will still generate
<figcaption>as expected
