diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml
new file mode 100644
index 0000000..db85f67
--- /dev/null
+++ b/.forgejo/workflows/ci.yml
@@ -0,0 +1,72 @@
+name: 'CI'
+
+on:
+  push:
+    branches:
+      - 'main'
+  pull_request:
+
+jobs:
+  test:
+    runs-on: docker
+
+    strategy:
+      matrix:
+        lisp:
+          - sbcl-bin
+
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Restore cache
+        id: restore-cache
+        uses: actions/cache/restore@v4
+        with:
+          path: |
+            ~/.quicklisp-client-fix
+            ~/.roswell
+            /usr/local/bin/ros
+            /usr/local/etc/roswell/
+            qlfile
+            qlfile.lock
+            .qlot
+            ~/.cache/common-lisp/
+          key: roswell-${{ runner.os }}-${{ matrix.lisp }}-${{ hashFiles('qlfile', 'qlfile.lock', '*.asd', '.forgejo/workflows/ci.yml') }}
+
+      - name: Install Roswell
+        if: steps.restore-cache.outputs.cache-hit != 'true'
+        env:
+          LISP: ${{ matrix.lisp }}
+        run: |
+          curl -L https://raw.githubusercontent.com/roswell/roswell/master/scripts/install-for-ci.sh | sh
+
+      - name: Install Qlot
+        if: steps.restore-cache.outputs.cache-hit != 'true'
+        run: |
+          ros install fukamachi/qlot
+
+      - name: Install dependencies
+        if: steps.restore-cache.outputs.cache-hit != 'true'
+        run: |
+          PATH="~/.roswell/bin:$PATH"
+          qlot install
+          qlot exec ros install hsx
+
+      - name: Save cache
+        id: save-cache
+        uses: actions/cache/save@v4
+        if: steps.restore-cache.outputs.cache-hit != 'true'
+        with:
+          path: |
+            ~/.quicklisp-client-fix
+            ~/.roswell
+            /usr/local/bin/ros
+            /usr/local/etc/roswell/
+            qlfile
+            qlfile.lock
+            .qlot
+            ~/.cache/common-lisp/
+          key: ${{ steps.restore-cache.outputs.cache-primary-key }}
+
+      - name: Run tests
+        run: .qlot/bin/rove hsx.asd
diff --git a/.github/workflows/test.yml b/.github/workflows/ci.yml
similarity index 91%
rename from .github/workflows/test.yml
rename to .github/workflows/ci.yml
index decfc7c..88f0c99 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/ci.yml
@@ -1,4 +1,4 @@
-name: 'test'
+name: 'CI'
 
 on:
   push:
@@ -7,15 +7,15 @@ on:
   pull_request:
 
 jobs:
-  tests:
+  test:
     runs-on: ubuntu-latest
-    
+
     strategy:
       matrix:
         lisp:
           - sbcl-bin
           - ccl-bin
-          
+
     env:
       LISP: ${{ matrix.lisp }}
 
diff --git a/README.md b/README.md
index b036ba3..d07a845 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
 # HSX
 
-HSX (Hypertext S-expression) is a simple and powerful HTML (Living Standard) generation library for Common Lisp.
+HSX (Hypertext S-expression) is a simple and powerful HTML (Living Standard)
+generation library for Common Lisp.
 
 This project is a fork of [ailisp/flute](https://github.com/ailisp/flute/).
 
@@ -8,13 +9,16 @@ This project is a fork of [ailisp/flute](https://github.com/ailisp/flute/).
 
 This software is still in ALPHA quality. The APIs are likely to change.
 
-Please check the [release notes](https://github.com/skyizwhite/hsx/releases) for updates.
+Please check the [release notes](https://code.skyizwhite.dev/paku/hsx/releases)
+for updates.
 
 ## Getting Started
 
 ### Basic Usage
 
-Use the `hsx` macro to create HTML elements. Attributes are specified using a property list after the element name, and child elements are nested directly inside.
+Use the `hsx` macro to create HTML elements. Attributes are specified using a
+property list after the element name, and child elements are nested directly
+inside.
 
 ```lisp
 (hsx
@@ -32,7 +36,8 @@ This generates:
 </div>
 ```
 
-To convert an HSX object into an HTML string, use the `render-to-string` function:
+To convert an HSX object into an HTML string, use the `render-to-string`
+function:
 
 ```lisp
 (render-to-string
@@ -43,7 +48,8 @@ To convert an HSX object into an HTML string, use the `render-to-string` functio
 
 HSX allows you to embed Common Lisp forms directly within your HTML structure.
 
-When working with HSX elements inside embedded Lisp forms, you should use the `hsx` macro again.
+When working with HSX elements inside embedded Lisp forms, you should use the
+`hsx` macro again.
 
 ```lisp
 (hsx
@@ -76,7 +82,8 @@ This might generate:
 
 ### Using Fragments
 
-To group multiple elements without adding an extra wrapper, use the fragment `<>`.
+To group multiple elements without adding an extra wrapper, use the fragment
+`<>`.
 
 ```lisp
 (hsx
@@ -96,7 +103,9 @@ This generates:
 
 ### Creating Components
 
-You can define reusable components using the `defcomp` macro. Component names must begin with a tilde (`~`). Properties should be declared using `&key`, `&rest`, or both. The body must return an HSX element.
+You can define reusable components using the `defcomp` macro. Component names
+must begin with a tilde (`~`). Properties should be declared using `&key`,
+`&rest`, or both. The body must return an HSX element.
 
 ```lisp
 (defcomp ~card (&key title children)
@@ -140,5 +149,3 @@ This project is licensed under the MIT License.
 © 2024 skyizwhite
 
 © 2018 Bo Yao
-
-Feel free to contribute to the project and report any issues or feature requests on the [GitHub repository](https://github.com/skyizwhite/hsx).
diff --git a/qlfile b/qlfile
index 4f02666..6b6b356 100644
--- a/qlfile
+++ b/qlfile
@@ -1,6 +1,6 @@
 ql alexandria
 ql cl-str
 
-github rove fukamachi/rove
-github dissect Shinmera/dissect ; workaround
-ql mstrings
+git rove https://github.com/fukamachi/rove
+git dissect https://github.com/Shinmera/dissect ; workaround
+git mstrings https://git.sr.ht/~shunter/mstrings
diff --git a/qlfile.lock b/qlfile.lock
index 8cadf24..41f11ce 100644
--- a/qlfile.lock
+++ b/qlfile.lock
@@ -1,24 +1,24 @@
 ("quicklisp" .
  (:class qlot/source/dist:source-dist
   :initargs (:distribution "https://beta.quicklisp.org/dist/quicklisp.txt" :%version :latest)
-  :version "2023-10-21"))
+  :version "2024-10-12"))
 ("alexandria" .
  (:class qlot/source/ql:source-ql
   :initargs (:%version :latest)
-  :version "ql-2023-10-21"))
+  :version "ql-2024-10-12"))
 ("cl-str" .
  (:class qlot/source/ql:source-ql
   :initargs (:%version :latest)
-  :version "ql-2023-10-21"))
+  :version "ql-2024-10-12"))
 ("rove" .
- (:class qlot/source/github:source-github
-  :initargs (:repos "fukamachi/rove" :ref nil :branch nil :tag nil)
-  :version "github-cacea7331c10fe9d8398d104b2dfd579bf7ea353"))
+ (:class qlot/source/git:source-git
+  :initargs (:remote-url "https://github.com/fukamachi/rove")
+  :version "git-cacea7331c10fe9d8398d104b2dfd579bf7ea353"))
 ("dissect" .
- (:class qlot/source/github:source-github
-  :initargs (:repos "Shinmera/dissect" :ref nil :branch nil :tag nil)
-  :version "github-a70cabcd748cf7c041196efd711e2dcca2bbbb2c"))
+ (:class qlot/source/git:source-git
+  :initargs (:remote-url "https://github.com/Shinmera/dissect")
+  :version "git-a70cabcd748cf7c041196efd711e2dcca2bbbb2c"))
 ("mstrings" .
- (:class qlot/source/ql:source-ql
-  :initargs (:%version :latest)
-  :version "ql-2023-10-21"))
+ (:class qlot/source/git:source-git
+  :initargs (:remote-url "https://git.sr.ht/~shunter/mstrings")
+  :version "git-7a94c070141c7cd03bbd3648b17724c3bf143393"))