README
Refined Starter Guide
This project refines and re-implements the excellent sanonz/hexo-theme-concise with Astro. Credit to the original theme; ideas, structure and styles are adapted for Astro’s content collections and routing.
⚡ Quick Start
Start here with the most common tasks:
- Clone and install
git clone https://github.com/loliblogs/Refinedpnpm install
- Start the dev server
pnpm dev
Open http://localhost:3000
.
- Build and preview
pnpm build && pnpm preview
Open http://localhost:4321
.
- Sync types (when content collections or schemas change; dev/build run type generation automatically)
pnpm sync
Note: dev/build already generate types; this is mainly for CI or after schema changes.
More commands are listed in the table below.
Command | Action |
---|---|
pnpm install | Installs dependencies |
pnpm dev | Starts local dev server at localhost:3000 |
pnpm dev:full | Starts dev server with a full cold start (no cache) |
pnpm build | Build your production site to ./dist/ |
pnpm build:full | Full clean build to ./dist/ and regenerate Pagefind index |
pnpm sync | Synchronize Astro-generated types |
pnpm preview | Preview your build locally at localhost:4321 |
pnpm typecheck | Run type checks via Astro (Including .astro files!) |
pnpm lint | Lint project files with ESLint |
pnpm lint:fix | Automatically fix fixable ESLint issues |
pnpm astro ... | Run CLI commands like astro add , astro db |
pnpm astro -- --help | Get help using the Astro CLI |
🌐 Browser Support
- Chrome 99+
- Firefox 97+
- Safari 15+ (iOS 15+)
Notes:
- Windows: Works on Windows 7 and newer. Use the latest Chrome/Firefox available for Win7.
- Android: Chrome requires Android 6.0+; Firefox works on Android 5.0+.
🚀 Project Structure
Inside of your Astro project, you’ll see the following folders and files:
/├── astro.config.ts├── eslint.config.ts├── tsconfig.json├── pagefind.yaml├── wrangler.json├── package.json├── pnpm-lock.yaml├── pnpm-workspace.yaml├── public/├── db/│ └── config.ts└── src/ ├── content.config.ts ├── env.d.ts ├── assets/ ├── components/ │ └── pages/ ├── config/ │ ├── site.config.tsx │ └── comments.config.tsx ├── content/ │ ├── post/ │ ├── oi/ │ ├── page/ │ └── oiPage/ ├── layouts/ │ ├── BaseLayout.astro │ └── RSSLayout.astro ├── pages/ │ ├── index.astro │ ├── [...slug].astro │ ├── category/ │ ├── tag/ │ ├── oi/ │ ├── rss.xml.ts │ └── rss-styles.xsl.ts ├── plugins/ ├── styles/ ├── types/ └── utils/
Astro uses file-based routing: any .astro, .md, .mdx, or .ts file under src/pages
is exposed as a route based on its file path, and nested directories become nested URLs. Dynamic and catch-all routes such as src/pages/[...slug].astro
and pagination patterns like **/[page].astro
are supported. Server endpoints implemented as .ts
files (for example src/pages/rss.xml.ts
) export a handler returning a Response
and are emitted as dynamic assets.
Content Collections are defined in src/content.config.ts
via defineCollection
and Zod schemas; each collection maps to a folder under src/content/<collection>
where Markdown/MDX entries provide frontmatter validated at dev/build time. Strong types are generated for safe access and can be synchronized with pnpm sync
, and content is queried with utilities like getCollection('post')
in pages and components.
This project keeps routes thin in src/pages
, implements reusable page templates under src/components/pages
, shares layout chrome in src/layouts
, centralizes site settings in src/config
, and places rendering and taxonomy helpers in src/utils
. Put only unreferenced files in public
; any asset imported from code or Markdown (including images) can live under src
(for example src/assets
) and will be compressed and bundled. In Markdown/MDX, images can be referenced via relative paths or imports; any import-resolvable alias works, including @/
which maps to src
(configured in tsconfig.json
).
📝 Frontmatter
Content frontmatter is validated by src/content.config.ts
. Collections post
and oi
share the same schema (title, dates, categories/tags and meta), while page
and oiPage
only require minimal fields like title
and optional dates. The example below shows a typical Markdown entry with inline comments explaining each field. Both singular and plural keys are accepted for categories and tags, and multiple hierarchical values are supported by using /
in a path-like notation.
---title: Building a Fast Astro Blogdescription: Optional, used for SEO summaries.excerpt: Optional, short teaser for lists.date: 2025-03-10updated: 2025-09-15
# You can use either `category` or `categories`. Multiple values are supported.# Hierarchies are written with slashes, and you can mix multiple hierarchies.categories: - Frontend/React - DevOps/Kubernetes
# You can use either `tags` or `tag`. Multiple and hierarchical tags are supported.tags: - javascript - react/hooks - kubernetes/networking
# Draft posts are rendered but excluded from production indexes/lists (e.g., home, archive).draft: false
# Sticky is optional. Omitted defaults to 0. Higher numbers surface earlier; negatives push to the bottom.sticky: 10
# Optional author override; falls back to site defaults if omitted.author: Jane Doe
# Enables comment system if configured for the site.comments: true
## encryption-related fields are documented in a dedicated section later.---
Intro paragraph for index pages.
Note: The read-more marker below only affects the home index when no `excerpt` is set in frontmatter.
::more
Remaining content…
Excerpts and previews:
- description: Optional SEO summary; auto-generated from body if omitted.
- excerpt: If present, used on all list pages (home/tag/category). If both
excerpt
and a read-more marker exist, excerpt wins (marker ignored). If absent: Home renders from body (before the first read-more marker if present; otherwise full body). Tag/Category pages show no body preview. If multiple read-more markers exist, only the first is respected.
Read-more markers (use one of the following):
::more
Recommended syntax that works in both .md
and .mdx
files.
<!--more-->
Legacy syntax that only works in .md
files; not supported in .mdx
due to MDX parsing.
For a comprehensive guide to Markdown syntax and features supported in this project, see the live example: Markdown Syntax Guide
This frontmatter maps directly to the Zod schemas in src/content.config.ts
. Dates are coerced to Date
. Categories and tags accept either a single string or an array; during processing they are normalized to arrays, and each string may represent a multi-level path such as Frontend/React
. Draft entries still render but are hidden from index-style pages (like the home and archives) in production. sticky
is optional (default 0). It controls pinning and sort priority in lists: larger values appear earlier; negative values explicitly sink to the bottom.
🧩 MDX (Markdown + JSX)
MDX lets you write Markdown and JSX in the same file. This project enables MDX via @astrojs/mdx
(see astro.config.ts
). A few quick notes:
- MDX is supported alongside Markdown in content collections under
src/content/**
. You can typically rename existing.md
files to.mdx
and they will almost always work as‑is; the same frontmatter schema and generated types apply. - The same remark/rehype pipeline runs for MDX, including math support and Shiki syntax highlighting. See the MathJax section for details.
- You can import and render
.astro
or.tsx
components in MDX. Interactive components follow Astro’s standard client directives when needed.
Example:
---title: Using MDX---
export const date = '2025-09-18' // frontmatter-equivalent; can be used for computations with {} belowexport const tags = ['mdx', 'tips'] // frontmatter-equivalent; can be used for computations
import MediumZoom from '@/components/MediumZoom.tsx'
# Hello MDX
Published on {new Date(date).toLocaleDateString()}
Tags: {tags.join(', ')}
<MediumZoom />
```jsconsole.log('fenced code is syntax highlighted');```
Learn more:
- Astro guide: MDX integration
- MDX official docs: mdxjs.com/docs
⚙ Configuration
Start with astro.config.ts
. Set site
to your canonical URL. This project now supports non-root bases (for example base: '/blog/'
). Per-collection subpaths are handled via basePath
in src/config/site.config.tsx
.
Important rules for basePath
:
- Provide a bare segment without leading or trailing slashes. Use
''
(empty) for the main blog, and for sections use values like'oi'
(not/oi
and notoi/
). - Paths are composed as:
import.meta.env.BASE_URL
+basePath
. For example, withbase = '/blog/'
andbasePath = 'oi'
, URLs become/blog/oi/...
.
The primary configuration lives in src/config/site.config.tsx
. It provides per‑collection settings (e.g., post
and oi
) including site identity (title, subtitle, description, language), navigation menu, pagination, the collection‑level basePath
, a global comments toggle, favicon links, and sensible defaults for encrypted content prompts. The file is heavily documented—skim the top for key options and consult the inline comments for the complete list.
Comments are wired through src/config/comments.config.tsx
. Despite the name, this file is a component entry point, not a rigid schema: you can replace its contents with any comment provider (Giscus, Disqus, Gitalk, or a custom widget). It is rendered by the post page and respects comments
in site.config.tsx
; use the site’s language
from getSiteConfig()
to localize your widget. If you swap providers, keep the default export as a React component.
🔐 Encryption
Encrypted posts use a password‑derived key to protect the rendered HTML. On the server, a 32‑byte key is derived with Argon2id (memory 64MB, time 3, parallelism 1) from a per‑post password and a unique salt, then the content is encrypted using AES‑256‑GCM with a random 12‑byte nonce. The encrypted payload is emitted as JSON that includes the salt, nonce and ciphertext with an authentication tag. On the client, a small web worker derives the same key from the password and salt, and the browser WebCrypto API performs authenticated decryption locally.
Implementation lives in src/utils/encrypt-processor.ts
and src/components/EncryptWrapper.astro
for server‑side encryption, src/utils/argon2-worker.ts
and src/components/DecryptClient.tsx
for client‑side derivation and decryption, src/utils/argon2-cache.ts
for server key derivation and caching, and db/config.ts
for the Astro DB table that stores encrypted derived keys and metadata. The post page integrates these pieces in src/components/pages/PostPage.astro
.
To mark a post as protected, set encryption fields in frontmatter. The password itself is never stored in content; instead, secrets supply it at build or runtime.
---title: Encrypted Exampledate: 2025-09-15encrypted: true# Optional UI text; both fields are optional and fall back to site defaults# defined in src/config/site.config.tsxprompt: This content is protected. Enter the password to view.hint: The password you received privately.---
Body content will be encrypted at build time when served to the client.
Server‑side secrets are read with getSecret()
and must be provided via environment variables. On first install, the postinstall
script attempts to copy .env.example
to .env
if .env
is missing; otherwise copy it manually. Only .env
is loaded locally. Define SECRET_PASSWORDS
, SECRET_ENCRYPTION_PASSWORD
and SECRET_ENCRYPTION_SALT
. SECRET_PASSWORDS
is a JSON object mapping <collection>:<entry id>
to the clear‑text password used to derive the per‑post key; the entry id includes the file extension relative to its collection directory (for example src/content/post/encrypted-test.mdx
has the id encrypted-test.mdx
). SECRET_ENCRYPTION_PASSWORD
and SECRET_ENCRYPTION_SALT
are used to HKDF‑derive an AES‑GCM key that encrypts derived keys at rest in the Astro DB cache.
# Map content entries to their passwords. Keys are "<collection>:<entry id>".# Examples:# post:encrypted-test.mdx → src/content/post/encrypted-test.mdx# oi:binary-search.md → src/content/oi/binary-search.mdSECRET_PASSWORDS='{"post:encrypted-test.mdx":"change-me","oi:binary-search.md":"change-me-too"}'
# Used to derive an AES-GCM key (via HKDF) for encrypting cached derived keys.SECRET_ENCRYPTION_PASSWORD='please-generate-a-strong-random-string'SECRET_ENCRYPTION_SALT='another-strong-random-string-or-base64'
For local development, place these in .env
(the .env.example
file is just a template and not loaded). For Cloudflare Pages, configure the same names as project secrets (for example with wrangler pages secret put SECRET_PASSWORDS
, and likewise for the other two). Secrets are never sent to the browser; only encrypted payloads are shipped, and decryption happens client‑side using the user‑entered password.
🧮 MathJax
Math rendering is handled in two layers. For Markdown/MDX content, math is parsed at the remark/rehype stage and converted to static HTML using a customized pipeline in src/plugins
. The remark plugin remark-math.ts
mirrors the official plugin and adds one small extension: when a single‑line inline math segment is wrapped by sufficiently wide $
delimiters (configurable via flowSingleLineMinDelimiter
), it is promoted to display mode by setting the resulting node’s class to math-display
. The rehype plugin rehype-mathjax.ts
keeps the same external behavior as upstream but is re‑implemented on top of MathJax v4; it converts math (including fenced ```math code blocks) to HTML, injects the generated stylesheet without font URLs, and defers font‑face CSS to src/styles/mathjax.css
(imported by src/styles/global.css
) for easier bundling.
For UI chrome like the Navigation and Sidebar, math may appear in dynamic strings (titles, excerpts, or generated lists). Those are processed by a singleton renderer in src/utils/mathjax-singleton.ts
. It lazily initializes a single MathJax instance, caches processed HTML in production, and applies the same font‑CSS cleanup so that UI chrome matches the content pipeline.
👀 Want to learn more?
Check the Astro documentation or join the Astro Discord. For this project specifically, please use Issues for reproducible bugs, regressions, and well‑scoped feature requests. Use Discussions for questions, how‑to help, ideas, design proposals, and general feedback—this keeps the issue tracker focused and actionable.
This is a personal project. Here, “personal” means good‑enough for my own use rather than a general‑purpose theme. To keep the code small, readable, and easy to maintain, some requests are intentionally out of scope:
- No i18n or multi‑locale routing. The
language
field only localizes built‑in UI text. - No feature toggle for MathJax; the math pipeline is built‑in. Disabling it requires code edits.
- No plugin system, theme preset matrix, or exhaustive switches; defaults are opinionated.
- No multi‑collection management scaffolding/UI; defining, adding, and removing collections follow the same manual, schema‑based pattern and are out of scope.
- Over‑engineered changes that add complexity without clear day‑to‑day value are likely to be declined.
Note: The items above are illustrative rather than exhaustive; they include—but are not limited to—the examples listed here.
Bug fixes and small, focused improvements are welcome. For non‑trivial changes, please open a Discussion first to confirm fit. If a proposal is declined due to project philosophy or scope, the author may—at their discretion—offer pointers to relevant files/modules, high‑level direction, or suggested edits to help you implement the change yourself; this is a best‑effort courtesy, not an obligation. If the current direction doesn’t meet your needs, feel free to fork under the LICENSE.