├── .github └── workflows │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── README.zh.md ├── biome.json ├── components ├── cf │ ├── BlockBox.tsx │ ├── CaptchaBox.tsx │ ├── ErrorBox.tsx │ ├── Footer.tsx │ ├── ui │ │ ├── CFCard.tsx │ │ ├── CFCardWrapper.tsx │ │ ├── NetworkLine.tsx │ │ ├── NetworkNode.tsx │ │ ├── NetworkStatusBox.tsx │ │ ├── NetworkStatusWrapper.tsx │ │ └── PageWrapper.tsx │ └── utils │ │ └── index.ts ├── home │ ├── Hero.tsx │ ├── HomeFooter.tsx │ └── ui │ │ ├── card-item.tsx │ │ └── card-section.tsx ├── layout │ ├── BaseLayout.tsx │ └── CFLayout.tsx ├── primitives.ts ├── providers.tsx ├── theme-switch.tsx └── ui │ ├── color-scheme.tsx │ └── icon.tsx ├── config ├── fonts.ts ├── home.ts ├── i18n.ts ├── icons.ts ├── routes.ts └── site.ts ├── docs └── assets │ ├── block-from-ip-dark.png │ ├── block-from-ip-light.png │ ├── captcha-ic-dark.png │ ├── captcha-ic-light.png │ ├── error-500s-dark.png │ ├── error-500s-light.png │ └── home.png ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── 404.tsx ├── _app.tsx ├── _document.tsx ├── cf │ └── [directory] │ │ └── [type].tsx └── index.tsx ├── postcss.config.js ├── public ├── favicon.svg └── robots.txt ├── scripts └── assets-process.ts ├── styles └── globals.css ├── tailwind.config.js ├── tsconfig.json ├── types └── index.ts └── utils └── console.ts /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | paths: 6 | - "package.json" 7 | - ".github/workflows/release.yml" 8 | branches: 9 | - main 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | discussions: write 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: Set environment variables 25 | id: env_vars 26 | run: | 27 | echo "repo_name=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT 28 | echo "commit_url=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commit" >> $GITHUB_OUTPUT 29 | echo "profile_url=${GITHUB_SERVER_URL}" >> $GITHUB_OUTPUT 30 | 31 | - name: Get version from package.json 32 | id: package_version 33 | run: | 34 | echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT 35 | 36 | - name: Check if release exists 37 | id: check_release 38 | run: | 39 | latest_release=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest || echo '{"tag_name":"none"}') 40 | latest_version=$(echo $latest_release | jq -r .tag_name | sed 's/^v//') 41 | current_version=${{ steps.package_version.outputs.version }} 42 | 43 | if [ "$latest_version" == "$current_version" ]; then 44 | echo "version_exists=true" >> $GITHUB_OUTPUT 45 | else 46 | echo "version_exists=false" >> $GITHUB_OUTPUT 47 | fi 48 | 49 | - name: Generate changelog 50 | if: steps.check_release.outputs.version_exists == 'false' 51 | id: changelog 52 | env: 53 | COMMIT_URL: ${{ steps.env_vars.outputs.commit_url }} 54 | PROFILE_URL: ${{ steps.env_vars.outputs.profile_url }} 55 | run: | 56 | CHANGELOG="" 57 | AUTHORS="" 58 | 59 | # Function to add emoji based on commit message 60 | add_emoji() { 61 | local msg="$1" 62 | local hash="$2" 63 | case "$msg" in 64 | *feat*|*特性*) echo "- ✨ [\`$msg\`]($COMMIT_URL/$hash)" ;; 65 | *fix*|*修复*) echo "- 🐛 [\`$msg\`]($COMMIT_URL/$hash)" ;; 66 | *docs*|*文档*) echo "- 📚 [\`$msg\`]($COMMIT_URL/$hash)" ;; 67 | *style*|*样式*) echo "- 💎 [\`$msg\`]($COMMIT_URL/$hash)" ;; 68 | *refactor*|*重构*) echo "- ♻️ [\`$msg\`]($COMMIT_URL/$hash)" ;; 69 | *perf*|*性能*) echo "- ⚡️ [\`$msg\`]($COMMIT_URL/$hash)" ;; 70 | *test*|*测试*) echo "- ✅ [\`$msg\`]($COMMIT_URL/$hash)" ;; 71 | *chore*|*构建*) echo "- 🔧 [\`$msg\`]($COMMIT_URL/$hash)" ;; 72 | *) echo "- 🔨 [\`$msg\`]($COMMIT_URL/$hash)" ;; 73 | esac 74 | } 75 | 76 | # Get all commits and authors since last tag 77 | LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || git rev-list --max-parents=0 HEAD) 78 | while IFS= read -r line; do 79 | HASH=$(echo "$line" | cut -f1) 80 | MSG=$(echo "$line" | cut -f2) 81 | CHANGELOG="$CHANGELOG$(add_emoji "$MSG" "$HASH")\n" 82 | done < <(git log --pretty=format:"%H%x09%s" $LAST_TAG..HEAD) 83 | 84 | # Get unique authors 85 | AUTHORS="\n\n## 👥 贡献者\n\n" 86 | while IFS= read -r author; do 87 | AUTHORS="$AUTHORS- [@$author]($PROFILE_URL/$author)\n" 88 | done < <(git log $LAST_TAG..HEAD --format="%aN" | sort -u) 89 | 90 | echo "CHANGELOG<> $GITHUB_OUTPUT 91 | echo -e "$CHANGELOG$AUTHORS" >> $GITHUB_OUTPUT 92 | echo "EOF" >> $GITHUB_OUTPUT 93 | 94 | - name: Create Release 95 | if: steps.check_release.outputs.version_exists == 'false' 96 | uses: softprops/action-gh-release@v1 97 | env: 98 | VERSION: ${{ steps.package_version.outputs.version }} 99 | REPO_NAME: ${{ steps.env_vars.outputs.repo_name }} 100 | with: 101 | tag_name: v${{ env.VERSION }} 102 | name: 🚀 Release v${{ env.VERSION }} 103 | body: | 104 | ## 📝 更新日志 105 | 106 | ${{ steps.changelog.outputs.CHANGELOG }} 107 | 108 | draft: false 109 | prerelease: false 110 | generate_release_notes: true 111 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | .next 39 | 40 | # Environment variables 41 | .env 42 | .env.local 43 | .env.development.local 44 | .env.test.local 45 | 46 | # Environment variables 47 | .env 48 | .env.local 49 | 50 | .DS_Store 51 | */.DS_Store 52 | **/.DS_Store 53 | 54 | out/ 55 | old/ 56 | 57 | bun.lock 58 | *.md 59 | !README* 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | cloudflare-custom-pages-nextjs Copyright (C) 2025 Alice39s 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌐 Cloudflare Custom Pages - Next.js 2 | 3 | A set of elegant, out-of-the-box Cloudflare WAF Custom Page Templates implemented using **Next.js**, **Tailwind CSS**, and **HeroUI**. Built with Next.js 15, TypeScript, and Tailwind CSS, featuring responsive design, dark mode support, and automatic Cloudflare variable replacement. 4 | 5 | English | [简体中文](README.zh.md) | [Online Demo](https://cw-preview.000000039.xyz/) 6 | 7 | > [!TIP] 8 | > Please comply with the project's [Open Source License](LICENSE) when making modifications. 9 | 10 | ## 📸 Screenshots 11 | 12 |
13 | Preview 14 |
Main Page 15 |
16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
Example PageLight ModeDark Mode
IP Block
JS Challenge
500s Error
44 |
45 | 46 | ## ✨ Key Features 47 | 48 | - 🎨 **Modern Responsive Design**: Sleek and contemporary layout optimized for all devices. 49 | - 🌙 **Dark Mode Support**: Automatically adapts to system preferences for comfortable viewing. 50 | - 🔒 **Full Coverage of Cloudflare Page Types**: 51 | - `Block`: WAF interception pages 52 | - `Error`: 1000s / 500s error pages 53 | - `Captcha`: CAPTCHA challenge pages 54 | - 📱 **Mobile-First Approach**: Guaranteed smooth experience on mobile devices. 55 | - 🎭 **Automatic Cloudflare Variable Replacement**: Seamless integration of Cloudflare-specific variables. 56 | 57 | ## 🛠️ Tech Stack 58 | 59 | - **Next.js 15** + **React 19** 60 | - **HeroUI v2** + **Tailwind CSS v3** 61 | - **TypeScript** 62 | 63 | ## 🎯 Supported Variables 64 | 65 | Currently supported automatic variable replacements: 66 | 67 | - `::CLIENT_IP::` - Client IP Address 68 | - `::RAY_ID::` - Cloudflare Ray ID 69 | - `::GEO::` - Client Geolocation 70 | - `::CLOUDFLARE_ERROR_500S_BOX::` - 500s Error Page Component 71 | - `::CLOUDFLARE_ERROR_1000S_BOX::` - 1000s Error Page Component 72 | - `::CAPTCHA_BOX::` - Cloudflare CAPTCHA Component 73 | - `::IM_UNDER_ATTACK_BOX::` - Cloudflare JavaScript Challenge Component 74 | 75 | ## 🔭 Usage 76 | 77 | Quickly access Cloudflare Custom Pages via [this link](https://dash.cloudflare.com/?to=/:account/:zone/custom-pages). 78 | 79 | > [!TIP] 80 | > Your Cloudflare zone must be on Pro plan or higher to use these templates. 81 | 82 | | Type | Subtype | Link | 83 | | ----------- | ------------------------------------------- | ------------------------------------ | 84 | | Error Pages | Server Errors (500s) | [Import Link][error-500s] | 85 | | | CF 1000s Errors | [Import Link][error-1000s] | 86 | | Block Pages | IP Block (1006) | [Import Link][block-ip] | 87 | | | WAF Block (1010) | [Import Link][block-waf] | 88 | | | Rate Limit Block (429) | [Import Link][block-rate-limit] | 89 | | Challenges | Interactive Challenge | [Import Link][challenge-interactive] | 90 | | | Managed Challenge (I'm Under Attack Mode™) | [Import Link][challenge-managed] | 91 | | | Country/Region Challenge | [Import Link][challenge-country] | 92 | | | JavaScript Challenge | [Import Link][challenge-js] | 93 | 94 | [error-500s]: https://cw-preview.000000039.xyz/cf/error/500s/ 95 | [error-1000s]: https://cw-preview.000000039.xyz/cf/error/1000s/ 96 | [block-ip]: https://cw-preview.000000039.xyz/cf/block/ip/ 97 | [block-waf]: https://cw-preview.000000039.xyz/cf/block/waf/ 98 | [block-rate-limit]: https://cw-preview.000000039.xyz/cf/block/rate-limit/ 99 | [challenge-interactive]: https://cw-preview.000000039.xyz/cf/challenge/interactive/ 100 | [challenge-managed]: https://cw-preview.000000039.xyz/cf/challenge/managed/ 101 | [challenge-country]: https://cw-preview.000000039.xyz/cf/challenge/country/ 102 | [challenge-js]: https://cw-preview.000000039.xyz/cf/challenge/javascript/ 103 | 104 | ## 🌍 Deployment Guide 105 | 106 | - **Deploy to Vercel (Recommended)**: 107 | 108 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FAlice39s%2Fcloudflare-custom-pages-nextjs%2Ftree%2Fmain&project-name=cloudflare-custom-pages-nextjs-fork&repository-name=cloudflare-custom-pages-nextjs-fork&demo-title=Online%20Demo&demo-description=A%20beautiful%2C%20out-of-the-box%20Cloudflare%20WAF%20custom%20page%20template.&demo-url=https%3A%2F%2Fcw-preview.000000039.xyz%2F) 109 | 110 | - **Manual Deployment**: 111 | 112 | ```bash 113 | bun run build 114 | # Ignore the following command if using Nginx/etc. 115 | bun run start 116 | ``` 117 | 118 | `bun run start` launches a local server using `serve@latest`, listening on `0.0.0.0:3001` by default. 119 | 120 | ## 🚀 Development Guide 121 | 122 | 0. **Install Bun**: 123 | 124 | ```bash 125 | # macOS/Linux: 126 | curl -fsSL https://bun.sh/install | bash 127 | # Windows PowerShell: 128 | powershell -c "irm bun.sh/install.ps1 | iex" 129 | ``` 130 | 131 | 1. **Clone Repository**: 132 | 133 | ```bash 134 | git clone https://github.com/Alice39s/cloudflare-custom-pages-nextjs.git 135 | ``` 136 | 137 | 2. **Install Dependencies**: 138 | 139 | ```bash 140 | bun install 141 | ``` 142 | 143 | 3. **Start Dev Server**: 144 | 145 | ```bash 146 | bun dev 147 | ``` 148 | 149 | 4. **Build Production Version**: 150 | 151 | ```bash 152 | bun run build 153 | ``` 154 | 155 | ## 🎨 Customization Guide 156 | 157 | ### 1. Site Configuration 158 | 159 | Modify fields in `./config/site.ts` (name, description, etc.). 160 | 161 | ### 2. Content Customization 162 | 163 | Translations of all texts can be modified in `. /config/i18n.ts` to change the translation of all texts (TODO: multi-language support). 164 | 165 | To change the page text, edit `. /config/i18n.ts`: 166 | 167 | ```ts 168 | export const blockPageTranslations = { 169 | ip: { 170 | title: "Access Denied - IP Blocked", 171 | message: "The owner of this website has banned your IP address.", 172 | }, 173 | // ... more translations 174 | }; 175 | ``` 176 | 177 | To modify page configurations (without text content), edit `./config/routes.ts`: 178 | 179 | ```ts 180 | export const blockPages = { 181 | ip: { 182 | type: "ip", 183 | code: "1006", 184 | icon: "shield-ban", 185 | networkStatus: { 186 | clientStatus: "error", 187 | edgeStatus: "success", 188 | }, 189 | }, 190 | // ... more configurations 191 | }; 192 | ``` 193 | 194 | ### 3. Component Styling 195 | 196 | Project structure: 197 | 198 | ``` 199 | components/ 200 | ├── cf/ # 🌩️ Cloudflare Components 201 | ├── home/ # 🏠 Homepage Components 202 | └── layout/ # 🖼️ Global Layout Components 203 | ``` 204 | 205 | ### 4. Custom Icons 206 | 207 | This project utilizes the `lucide-react` icon library. To optimize the project's size, we have encapsulated a unified entry point and on-demand loading component called `Icon`. 208 | 209 | You can follow the steps below to add or replace icons: 210 | 211 | 1. Check if the icon you want to use is not already listed in `./config/icons.ts`. If it exists, skip directly to step 5. 212 | 213 | 2. Visit the [Lucide](https://lucide.dev/icons/) icon library and select your preferred icon. 214 | 215 | 3. Click the `Copy Component Name` button to copy the icon's name. 216 | 217 | 4. Then, navigate to `./config/icons.ts` and follow the instructions to add the icon name to: 218 | 219 | 1. `import { ... Component } from "lucide-react"` (import the icon component) 220 | 2. `export type IconKey = ...` (add the icon name to the type list) 221 | 3. `export const icons = { ... }` (add the icon name to the mapping dictionary) 222 | 223 | 5. Finally, use the desired icon in `./config/routes.ts`. 224 | 225 | ## 📜 License 226 | 227 | Licensed under GPL v3.0. See [LICENSE](LICENSE) for details. 228 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # 🌐 Cloudflare Custom Pages - Next.js 2 | 3 | 使用 **Next.js**、**Tailwind CSS** 和 **HeroUI** 实现一套美观的、开箱即用的 Cloudflare WAF 自定义页面模板。本模板基于 Next.js 15、使用 TypeScript 和 Tailwind CSS 开发,适配多种设备布局、支持深色模式、自动替换 Cloudflare 变量。 4 | 5 | 简体中文 | [English](README.md) | [Online Demo](https://cw-preview.000000039.xyz/) 6 | 7 | > [!TIP] 8 | > 二次开发时,请注意遵守本项目的 [开源许可证](LICENSE)。 9 | 10 | ## ✨ 主要特点 11 | 12 | - 🎨 **现代化响应式设计**:适配各种设备尺寸,使用 Polyfill 技术向前兼容老旧浏览器。 13 | - 🌙 **深色模式支持**:自动适配系统偏好,支持切换亮/暗色模式。 14 | - 🔒 **完整支持所有 Cloudflare 自定义页面类型**: 15 | - `Block`: WAF 拦截页面 16 | - `Error`: 1000s / 500s 错误页面 17 | - `Captcha`: CAPTCHA 质询页面 18 | - 🎭 **自动替换 Cloudflare 变量**:无缝集成 Cloudflare 特定变量到页面中。 19 | 20 | ## 📸 截图预览 21 | 22 |
23 | Preview 24 |
主页 25 |
26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
示例页面亮色模式暗色模式
IP 拦截
JS 质询
源站错误
54 |
55 | 56 | ## 🛠️ 技术栈 57 | 58 | - **Next.js 15** + **React 19** 59 | - **HeroUI v2** + **Tailwind CSS v3** 60 | - **TypeScript** 61 | 62 | ## 🎯 支持变量 63 | 64 | 目前本模板支持自动替换以下变量: 65 | 66 | - `::CLIENT_IP::` - 客户端 IP 地址 67 | - `::RAY_ID::` - Cloudflare Ray ID 68 | - `::GEO::` - 客户端地理位置 69 | - `::CLOUDFLARE_ERROR_500S_BOX::` - 500s 错误页面组件 70 | - `::CLOUDFLARE_ERROR_1000S_BOX::` - 1000s 错误页面组件 71 | - `::CAPTCHA_BOX::` - Cloudflare 的 CAPTCHA 组件 72 | - `::IM_UNDER_ATTACK_BOX::` - Cloudflare 的 JavaScript 挑战组件 73 | 74 | ## 🔭 使用指南 75 | 76 | 可快速点击 [这个链接](https://dash.cloudflare.com/?to=/:account/:zone/custom-pages) 快速跳转到 Cloudflare 的 Custom Pages 页面。 77 | 78 | > [!TIP] 79 | > 您的域必须购买 Pro 及以上的付费套餐才能使用本模板。 80 | 81 | | 类型 | 子类型 | 链接 | 82 | | -------- | ---------------------------------- | ------------------------------- | 83 | | 错误页面 | 服务器错误 500s | [传送门][error-500s] | 84 | | | CF 1000s 错误页面 | [传送门][error-1000s] | 85 | | 阻止页面 | IP 拦截页面 (1006) | [传送门][block-ip] | 86 | | | WAF 拦截页面 (1010) | [传送门][block-waf] | 87 | | | 速率限制拦截 (429) | [传送门][block-rate-limit] | 88 | | 验证页面 | 交互式质询 | [传送门][challenge-interactive] | 89 | | | 托管质询 (I'm Under Attack Mode™) | [传送门][challenge-managed] | 90 | | | 国家 (地区) 质询 | [传送门][challenge-country] | 91 | | | JavaScript 质询 | [传送门][challenge-js] | 92 | 93 | [error-500s]: https://cw-preview.000000039.xyz/cf/error/500s/ 94 | [error-1000s]: https://cw-preview.000000039.xyz/cf/error/1000s/ 95 | [block-ip]: https://cw-preview.000000039.xyz/cf/block/ip/ 96 | [block-waf]: https://cw-preview.000000039.xyz/cf/block/waf/ 97 | [block-rate-limit]: https://cw-preview.000000039.xyz/cf/block/rate-limit/ 98 | [challenge-interactive]: https://cw-preview.000000039.xyz/cf/challenge/interactive/ 99 | [challenge-managed]: https://cw-preview.000000039.xyz/cf/challenge/managed/ 100 | [challenge-country]: https://cw-preview.000000039.xyz/cf/challenge/country/ 101 | [challenge-js]: https://cw-preview.000000039.xyz/cf/challenge/javascript/ 102 | 103 | ## 🌍 部署指南 104 | 105 | - **部署到 Vercel (推荐)**: 106 | 107 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FAlice39s%2Fcloudflare-custom-pages-nextjs%2Ftree%2Fmain&project-name=cloudflare-custom-pages-nextjs-fork&repository-name=cloudflare-custom-pages-nextjs-fork&demo-title=Online%20Demo&demo-description=A%20beautiful%2C%20out-of-the-box%20Cloudflare%20WAF%20custom%20page%20template.&demo-url=https%3A%2F%2Fcw-preview.000000039.xyz%2F) 108 | 109 | - **自行部署**: 110 | 111 | ```bash 112 | bun run build 113 | # 如果您使用 Nginx 等程序,请忽略以下命令 114 | bun run start 115 | ``` 116 | 117 | `bun run start` 会使用 `serve@latest` 启动一个本地服务器,并默认监听 `0.0.0.0:3001`。 118 | 119 | ## 🚀 开发指南 120 | 121 | 0. **安装 Bun**: 122 | 123 | ```bash 124 | # macOS/Linux: 125 | curl -fsSL https://bun.sh/install | bash 126 | # Windows PowerShell: 127 | powershell -c "irm bun.sh/install.ps1 | iex" 128 | ``` 129 | 130 | 1. **下载项目**: 131 | 132 | ```bash 133 | git clone https://github.com/Alice39s/cloudflare-custom-pages-nextjs.git 134 | ``` 135 | 136 | 2. **安装依赖**: 137 | 138 | ```bash 139 | bun install 140 | ``` 141 | 142 | 3. **启动开发服务器**: 143 | 144 | ```bash 145 | bun dev 146 | ``` 147 | 148 | 4. **构建生产版本**: 149 | 150 | ```bash 151 | bun run build 152 | ``` 153 | 154 | ## 🎨 自定义指南 155 | 156 | ### 1. 自定义站点配置 157 | 158 | 可修改 `./config/site.ts` 中的 `name` 和 `description` 等字段。 159 | 160 | ### 2. 自定义文案 161 | 162 | 可在 `./config/i18n.ts` 中修改所有文案的翻译 (TODO: 支持多语言)。 163 | 164 | 如需修改页面文案,编辑 `./config/i18n.ts`: 165 | 166 | ```ts 167 | export const blockPageTranslations = { 168 | ip: { 169 | title: "Access Denied - IP Blocked", 170 | message: "The owner of this website has banned your IP address.", 171 | }, 172 | // ... 更多翻译 173 | }; 174 | ``` 175 | 176 | 如需修改页面配置(不含文案),编辑 `./config/routes.ts`: 177 | 178 | ```ts 179 | export const blockPages = { 180 | ip: { 181 | type: "ip", 182 | code: "1006", 183 | icon: "shield-ban", 184 | networkStatus: { 185 | clientStatus: "error", 186 | edgeStatus: "success", 187 | }, 188 | }, 189 | // ... 更多配置 190 | }; 191 | ``` 192 | 193 | ### 3. 自定义组件样式 194 | 195 | 项目结构如下: 196 | 197 | ``` 198 | components/ 199 | ├── cf/ # 🌩️ Cloudflare 页面组件 200 | ├── home/ # 🏠 首页组件 201 | └── layout/ # 🖼️ 全局布局组件 202 | ``` 203 | 204 | ### 4. 自定义图标 205 | 206 | 本项目使用 `lucide-react` 图标库,为了节省项目体积,封装了一个统一入口、按需引入的组件 `Icon` 。 207 | 208 | 你可以根据以下步骤添加/替换图标: 209 | 210 | 1. 在 `./config/icons.ts` 中确定你想要使用的图标不在清单中,如果已存在,则直接跳到第 5 步。 211 | 2. 前往 [Lucide](https://lucide.dev/icons/) 图标库,挑选你喜欢的图标。 212 | 3. 点击 `Copy Component Name` 按钮复制图标名称。 213 | 4. 随后前往 `./config/icons.ts` 按照指引依次将图标名称添加到: 214 | 1. `import { ... Component } from "lucide-react"` (引入图标组件) 215 | 2. `export type IconKey = ...` (添加图标名称到类型列表) 216 | 3. `export const icons = { ... }` (添加图标名称到映射字典) 217 | 5. 最后在 `./config/routes.ts` 中使用你想要的图标。 218 | 219 | ## 📜 许可证 220 | 221 | 本项目采用 GPL v3.0 许可证开源,详情请参阅 [LICENSE](LICENSE) 文件。 222 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": [] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "space", 15 | "indentWidth": 2 16 | }, 17 | "organizeImports": { 18 | "enabled": true 19 | }, 20 | "linter": { 21 | "enabled": true, 22 | "rules": { 23 | "recommended": true, 24 | "security": { 25 | "noDangerouslySetInnerHtml": "off" 26 | } 27 | } 28 | }, 29 | "javascript": { 30 | "formatter": { 31 | "quoteStyle": "double" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /components/cf/BlockBox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Icon } from "@/components/ui/icon"; 4 | import { blockPageTranslations } from "@/config/i18n"; 5 | import type { BlockPageConfig } from "@/config/routes"; 6 | import { CFCard } from "./ui/CFCard"; 7 | import { CFCardWrap } from "./ui/CFCardWrapper"; 8 | import { NetworkStatusBox } from "./ui/NetworkStatusBox"; 9 | import { NetworkStatusWrapper } from "./ui/NetworkStatusWrapper"; 10 | 11 | export const BlockBox = ({ 12 | type, 13 | code, 14 | icon, 15 | networkStatus, 16 | }: BlockPageConfig) => { 17 | const translation = blockPageTranslations[type]; 18 | return ( 19 | 20 | } 25 | headerClassName="bg-gradient-to-br from-red-50 to-red-100 dark:from-red-950 dark:to-gray-900" 26 | scheme="danger" 27 | > 28 |
29 |
30 |
31 |
32 | Your IP: 33 | ::CLIENT_IP:: 34 |
35 | 36 | {type === "waf" && ( 37 |
38 | Ray ID: 39 | ::RAY_ID:: 40 |
41 | )} 42 |
43 |
44 | 45 | 46 | 47 | 48 |
49 |
50 |
51 | ); 52 | }; 53 | 54 | export default BlockBox; 55 | -------------------------------------------------------------------------------- /components/cf/CaptchaBox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Icon } from "@/components/ui/icon"; 4 | import { challengePageTranslations } from "@/config/i18n"; 5 | import type { ChallengePageConfig } from "@/config/routes"; 6 | import { CFCard } from "./ui/CFCard"; 7 | import { CFCardWrap } from "./ui/CFCardWrapper"; 8 | import { NetworkStatusBox } from "./ui/NetworkStatusBox"; 9 | import { NetworkStatusWrapper } from "./ui/NetworkStatusWrapper"; 10 | 11 | export const CaptchaBox = ({ 12 | type, 13 | box, 14 | icon, 15 | networkStatus, 16 | }: ChallengePageConfig) => { 17 | const translation = challengePageTranslations[type]; 18 | return ( 19 | 20 | } 25 | headerClassName="bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/30 dark:to-blue-900/10" 26 | scheme="primary" 27 | > 28 |
29 | {translation.message && ( 30 |
31 | 35 |

36 | {translation.message} 37 |

38 |
39 | )} 40 | 41 |
42 | {box ? ( 43 |
::${box}::
` }} 46 | aria-live="polite" 47 | /> 48 | ) : ( 49 |

50 | Loading verification... 51 |

52 | )} 53 |
54 | 55 | 56 | 57 | 58 |
59 |
60 |
61 | ); 62 | }; 63 | 64 | export default CaptchaBox; 65 | -------------------------------------------------------------------------------- /components/cf/ErrorBox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Icon } from "@/components/ui/icon"; 4 | import { errorPageTranslations, interfaceTranslations } from "@/config/i18n"; 5 | import type { ErrorPageConfig } from "@/config/routes"; 6 | import { Chip } from "@heroui/react"; 7 | import { CFCard } from "./ui/CFCard"; 8 | import { CFCardWrap } from "./ui/CFCardWrapper"; 9 | import { NetworkStatusBox } from "./ui/NetworkStatusBox"; 10 | import { NetworkStatusWrapper } from "./ui/NetworkStatusWrapper"; 11 | 12 | export const ErrorBox = ({ 13 | type, 14 | code, 15 | box, 16 | icon, 17 | networkStatus, 18 | }: ErrorPageConfig) => { 19 | const translation = errorPageTranslations[type]; 20 | 21 | return ( 22 | 23 | 28 | Error {code} 29 | 30 | } 31 | icon={} 32 | scheme="danger" 33 | > 34 | 35 | 36 | 37 | 38 | {/* 隐藏的 Cloudflare 错误占位符,仅用于服务端注入 */} 39 | {type === "1000s" && ( 40 | 43 | )} 44 | 45 | {type === "500s" && ( 46 | 49 | )} 50 | 51 | {box && ( 52 |
53 |
54 |

55 | 56 | {interfaceTranslations["error-details"].message} 57 |

58 | 59 |
60 |
::${box}::
`, 63 | }} 64 | /> 65 |
66 |
67 |
68 | )} 69 |
70 |
71 | ); 72 | }; 73 | 74 | export default ErrorBox; 75 | -------------------------------------------------------------------------------- /components/cf/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardBody } from "@heroui/card"; 2 | import { memo, useCallback, useEffect, useMemo, useState } from "react"; 3 | import { CFCardWrap } from "./ui/CFCardWrapper"; 4 | import { countryCodeToFlag } from "./utils"; 5 | 6 | const useIsClient = () => { 7 | const [isClient, setIsClient] = useState(false); 8 | useEffect(() => setIsClient(true), []); 9 | return isClient; 10 | }; 11 | 12 | const useGeoLocation = () => { 13 | const [geoData, setGeoData] = useState({ text: "", flag: "🌍" }); 14 | 15 | const updateGeoData = useCallback(() => { 16 | const locationMetas = document.querySelectorAll( 17 | 'meta[name="location-code"]', 18 | ); 19 | let text = ""; 20 | for (const m of Array.from(locationMetas)) { 21 | const v = m.getAttribute("content"); 22 | if (v) { 23 | text = v; 24 | break; 25 | } 26 | } 27 | 28 | let newGeoData: { text: string; flag: string }; 29 | if (text && text.length === 2 && /^[A-Za-z]{2}$/.test(text)) { 30 | const flag = countryCodeToFlag(text); 31 | newGeoData = { text, flag }; 32 | } else { 33 | newGeoData = { text, flag: "🌍" }; 34 | } 35 | 36 | setGeoData((prev) => 37 | prev.text !== newGeoData.text || prev.flag !== newGeoData.flag 38 | ? newGeoData 39 | : prev, 40 | ); 41 | }, []); 42 | 43 | useEffect(() => { 44 | updateGeoData(); 45 | 46 | const observer = new MutationObserver(updateGeoData); 47 | observer.observe(document.head, { 48 | childList: true, 49 | subtree: true, 50 | attributes: true, 51 | attributeFilter: ["content"], 52 | }); 53 | 54 | const interval = setInterval(updateGeoData, 1000); 55 | 56 | return () => { 57 | observer.disconnect(); 58 | clearInterval(interval); 59 | }; 60 | }, [updateGeoData]); 61 | 62 | return geoData; 63 | }; 64 | 65 | interface InfoItemProps { 66 | label: string; 67 | value: string; 68 | flag?: string; 69 | isGeo?: boolean; 70 | } 71 | 72 | const InfoItem = memo( 73 | ({ label, value, flag, isGeo = false }: InfoItemProps) => ( 74 | 75 | {label}:{flag && {flag}} 76 | {value} 77 | 78 | ), 79 | ); 80 | 81 | InfoItem.displayName = "InfoItem"; 82 | 83 | const Separator = memo(() => ); 84 | Separator.displayName = "Separator"; 85 | 86 | export const FooterContent = memo(() => { 87 | const { text, flag } = useGeoLocation(); 88 | 89 | const geoDisplayValue = useMemo(() => { 90 | return text || "Unknown"; 91 | }, [text]); 92 | 93 | return ( 94 | 95 | 96 | 97 |
98 | 104 | 105 | 106 | 107 | 108 |
109 |
110 |
111 |
112 | ); 113 | }); 114 | FooterContent.displayName = "FooterContent"; 115 | 116 | export const Footer = memo(() => { 117 | const isClient = useIsClient(); 118 | 119 | if (!isClient) { 120 | return null; 121 | } 122 | 123 | return ; 124 | }); 125 | 126 | Footer.displayName = "Footer"; 127 | export default Footer; 128 | -------------------------------------------------------------------------------- /components/cf/ui/CFCard.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeSwitch } from "@/components/theme-switch"; 2 | import { getSchemeClasses } from "@/components/ui/color-scheme"; 3 | import type { ColorScheme } from "@/config/home"; 4 | import { Card, CardBody, CardFooter, CardHeader } from "@heroui/card"; 5 | import { clsx as cx } from "clsx"; 6 | import type { ReactNode } from "react"; 7 | 8 | interface CFCardProps { 9 | title: string; 10 | subtitle?: ReactNode; 11 | message: string; 12 | icon: ReactNode; 13 | watermark?: ReactNode; 14 | headerClassName?: string; 15 | iconClassName?: string; 16 | scheme?: ColorScheme; 17 | children?: ReactNode; 18 | footer?: ReactNode; 19 | } 20 | 21 | export const CFCard = ({ 22 | title, 23 | subtitle, 24 | message, 25 | icon, 26 | watermark, 27 | headerClassName = "bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900", 28 | iconClassName, 29 | scheme = "primary", 30 | children, 31 | footer, 32 | }: CFCardProps) => { 33 | const schemeClasses = getSchemeClasses(scheme); 34 | const finalIconClassName = 35 | iconClassName || 36 | cx("bg-gradient-to-br", schemeClasses.gradient, schemeClasses.iconBg); 37 | 38 | const getIconGlowClass = () => { 39 | if (scheme === "primary") return "bg-blue-500/20"; 40 | if (scheme === "danger") return "bg-red-500/20"; 41 | if (scheme === "warning") return "bg-amber-500/20"; 42 | return "bg-default-500/20"; 43 | }; 44 | 45 | return ( 46 | 50 | 53 | {watermark && ( 54 |
55 | {watermark} 56 |
57 | )} 58 | 59 |
60 | 61 |
62 | 63 |
64 |
65 |
66 |
72 |
78 |
79 | {icon} 80 |
81 |
82 |
83 |
84 | 85 |
86 | {subtitle && 87 | (typeof subtitle === "string" ? ( 88 | 89 | {subtitle} 90 | 91 | ) : ( 92 | subtitle 93 | ))} 94 |

95 | {title} 96 |

97 |
98 |
99 | 100 | 101 | 105 |

109 | {message} 110 |

111 | {children} 112 |
113 | 114 | {footer && ( 115 | 119 | {footer} 120 | 121 | )} 122 | 123 | ); 124 | }; 125 | -------------------------------------------------------------------------------- /components/cf/ui/CFCardWrapper.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | interface CFCardWrapProps { 4 | children: ReactNode; 5 | className?: string; 6 | } 7 | 8 | export const CFCardWrap = ({ children, className }: CFCardWrapProps) => { 9 | return ( 10 |
11 | {children} 12 |
13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /components/cf/ui/NetworkLine.tsx: -------------------------------------------------------------------------------- 1 | import { clsx } from "clsx"; 2 | 3 | type NetworkStatus = "success" | "error" | "challenging"; 4 | 5 | interface NetworkLineProps { 6 | status: NetworkStatus; 7 | } 8 | 9 | export const NetworkLine = ({ status }: NetworkLineProps) => { 10 | const styles = { 11 | success: { 12 | base: "bg-green-100/50 dark:bg-green-900/30", 13 | animation: "animate-flow", 14 | gradient: 15 | "bg-gradient-to-r from-transparent via-green-500/60 to-transparent dark:via-green-400/80", 16 | }, 17 | error: { 18 | base: "bg-red-100/50 dark:bg-red-900/30", 19 | animation: "animate-flow", 20 | gradient: 21 | "bg-gradient-to-r from-transparent via-red-500/60 to-transparent dark:via-red-400/80", 22 | }, 23 | challenging: { 24 | base: "bg-orange-100/50 dark:bg-orange-900/30", 25 | animation: "animate-network-loading", 26 | gradient: 27 | "bg-gradient-to-r from-transparent via-orange-500/60 to-transparent dark:via-orange-400/80", 28 | }, 29 | }[status]; 30 | 31 | return ( 32 |
33 |
39 |
46 |
47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /components/cf/ui/NetworkNode.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from "@/components/ui/icon"; 2 | import { clsx } from "clsx"; 3 | 4 | type NetworkStatus = "success" | "error" | "challenging"; 5 | 6 | interface NetworkNodeProps { 7 | label: string; 8 | status: NetworkStatus; 9 | className?: string; 10 | } 11 | 12 | export const NetworkNode = ({ label, status, className }: NetworkNodeProps) => { 13 | const styles = { 14 | success: { 15 | container: 16 | "bg-green-50/80 text-green-600 ring-1 ring-green-100/80 dark:bg-green-900/20 dark:text-green-300 dark:ring-green-900/30", 17 | icon: "text-green-500 dark:text-green-400", 18 | }, 19 | error: { 20 | container: 21 | "bg-red-50/80 text-red-600 ring-1 ring-red-100/80 dark:bg-red-900/20 dark:text-red-300 dark:ring-red-900/30", 22 | icon: "text-red-500 dark:text-red-400", 23 | }, 24 | challenging: { 25 | container: 26 | "bg-orange-50/80 text-orange-600 ring-1 ring-orange-100/80 dark:bg-orange-900/20 dark:text-orange-300 dark:ring-orange-900/30", 27 | icon: "text-orange-500 dark:text-orange-400", 28 | }, 29 | }[status]; 30 | 31 | const iconName = 32 | status === "success" 33 | ? "check-circle" 34 | : status === "error" 35 | ? "x-circle" 36 | : "shield-check"; 37 | 38 | return ( 39 |
47 | 55 | 61 | {label} 62 | 63 |
64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /components/cf/ui/NetworkStatusBox.tsx: -------------------------------------------------------------------------------- 1 | import { interfaceTranslations } from "@/config/i18n"; 2 | import type { NetworkStatusConfig } from "@/config/routes"; 3 | import { clsx } from "clsx"; 4 | import { useEffect, useState } from "react"; 5 | import { NetworkLine } from "./NetworkLine"; 6 | import { NetworkNode } from "./NetworkNode"; 7 | 8 | interface NetworkStatusBoxProps extends NetworkStatusConfig { 9 | rayId?: string; 10 | className?: string; 11 | } 12 | 13 | export const NetworkStatusBox = ({ 14 | clientStatus, 15 | edgeStatus, 16 | originStatus, 17 | className, 18 | }: NetworkStatusBoxProps) => { 19 | const [hostname, setHostname] = useState(""); 20 | 21 | useEffect(() => { 22 | const domain = window.location.hostname; 23 | if (domain.length <= 10) { 24 | setHostname(domain); 25 | } 26 | }, []); 27 | 28 | return ( 29 |
37 |
38 | 43 | 44 | 49 | 50 | {originStatus && ( 51 | <> 52 | 53 | 61 | 62 | )} 63 |
64 |
65 | ); 66 | }; 67 | 68 | export default NetworkStatusBox; 69 | -------------------------------------------------------------------------------- /components/cf/ui/NetworkStatusWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from "@/components/ui/icon"; 2 | import { interfaceTranslations } from "@/config/i18n"; 3 | import type { ReactNode } from "react"; 4 | 5 | export const NetworkStatusWrapper = ({ children }: { children: ReactNode }) => { 6 | return ( 7 |
8 |
9 |

10 | 14 | {interfaceTranslations["connection-tracking"].message} 15 |

16 | {children} 17 |
18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /components/cf/ui/PageWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { CFLayout } from "@/components/layout/CFLayout"; 2 | import { Providers } from "@/components/providers"; 3 | import { blockPages, challengePages, errorPages } from "@/config/routes"; 4 | import type { 5 | BlockPageConfig, 6 | ChallengePageConfig, 7 | ErrorPageConfig, 8 | } from "@/config/routes"; 9 | import type { PageType } from "@/config/routes"; 10 | import { useRouter } from "next/router"; 11 | import { BlockBox } from "../BlockBox"; 12 | import { CaptchaBox } from "../CaptchaBox"; 13 | import { ErrorBox } from "../ErrorBox"; 14 | 15 | type PageConfigMap = { 16 | error: { 17 | pages: typeof errorPages; 18 | defaultType: string; 19 | component: typeof ErrorBox; 20 | config: ErrorPageConfig; 21 | }; 22 | block: { 23 | pages: typeof blockPages; 24 | defaultType: string; 25 | component: typeof BlockBox; 26 | config: BlockPageConfig; 27 | }; 28 | challenge: { 29 | pages: typeof challengePages; 30 | defaultType: string; 31 | component: typeof CaptchaBox; 32 | config: ChallengePageConfig; 33 | }; 34 | }; 35 | 36 | const pageConfigs: { 37 | [K in PageType]: Omit; 38 | } = { 39 | error: { 40 | pages: errorPages, 41 | defaultType: "500s", 42 | component: ErrorBox, 43 | }, 44 | block: { 45 | pages: blockPages, 46 | defaultType: "ip", 47 | component: BlockBox, 48 | }, 49 | challenge: { 50 | pages: challengePages, 51 | defaultType: "interactive", 52 | component: CaptchaBox, 53 | }, 54 | }; 55 | 56 | export function PageWrapper({ pageType }: { pageType: PageType }) { 57 | const router = useRouter(); 58 | const { type } = router.query; 59 | const { pages, defaultType, component: Component } = pageConfigs[pageType]; 60 | const config = 61 | typeof type === "string" && type in pages 62 | ? pages[type as keyof typeof pages] 63 | : pages[defaultType as keyof typeof pages]; 64 | 65 | if (router.isFallback) { 66 | return null; 67 | } 68 | 69 | return ( 70 | 71 | 72 | {/* biome-ignore lint/suspicious/noExplicitAny: TypeScript Too HARD */} 73 | 74 | 75 | 76 | ); 77 | } 78 | 79 | export function getStaticPaths(pageType: PageType) { 80 | const { pages } = pageConfigs[pageType]; 81 | return { 82 | paths: Object.keys(pages).map((type) => ({ 83 | params: { type }, 84 | })), 85 | fallback: false, 86 | }; 87 | } 88 | 89 | export function getStaticProps(pageType: PageType, params: { type: string }) { 90 | const { pages } = pageConfigs[pageType]; 91 | const type = params.type; 92 | 93 | if (!(type in pages)) { 94 | return { 95 | notFound: true, 96 | }; 97 | } 98 | 99 | return { 100 | props: {}, 101 | }; 102 | } 103 | -------------------------------------------------------------------------------- /components/cf/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const countryCodeToFlag = (countryCode: string): string => { 2 | if (!countryCode || countryCode.length !== 2) return "🌍"; 3 | return String.fromCodePoint( 4 | ...[...countryCode.toUpperCase()].map( 5 | (char) => 127397 + char.charCodeAt(0), 6 | ), 7 | ); 8 | }; 9 | -------------------------------------------------------------------------------- /components/home/Hero.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from "@/components/ui/icon"; 2 | import { siteConfig } from "@/config/site"; 3 | import { clsx as cx } from "clsx"; 4 | 5 | export function Hero({ className }: { className?: string }) { 6 | return ( 7 |
13 |
14 |
15 | 19 |
20 |
21 |
22 |

23 | {siteConfig.name} 24 |

25 |

26 | {siteConfig.description} 27 |

28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/home/HomeFooter.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ThemeSwitch } from "@/components/theme-switch"; 4 | import { Icon } from "@/components/ui/icon"; 5 | import type { IconKey } from "@/config/icons"; 6 | import { siteConfig } from "@/config/site"; 7 | import print from "@/utils/console"; 8 | import { clsx } from "clsx"; 9 | import { memo } from "react"; 10 | import type { FC } from "react"; 11 | 12 | interface FooterLinkProps { 13 | href: string; 14 | icon: IconKey; 15 | label: string; 16 | } 17 | 18 | const FooterLink = memo(({ href, icon, label }) => ( 19 | 26 | 30 | 31 | )); 32 | 33 | FooterLink.displayName = "FooterLink"; 34 | 35 | const HomeFooter: FC<{ className?: string }> = ({ className }) => { 36 | const links: FooterLinkProps[] = [ 37 | { href: siteConfig.links.docs, icon: "book-open", label: "Documentation" }, 38 | { 39 | href: siteConfig.links.github, 40 | icon: "github", 41 | label: "GitHub Repository", 42 | }, 43 | ]; 44 | 45 | print(); 46 | 47 | return ( 48 |
54 |
55 |
56 |
57 | © {new Date().getFullYear()} Alice39s 58 |
59 | 60 |
61 |
62 | {links.map((link) => ( 63 | 64 | ))} 65 |
66 |
67 | 68 |
69 | 70 |
71 | Made with 72 | 73 | by Alice39s 74 |
75 |
76 |
77 |
78 | ); 79 | }; 80 | 81 | export default HomeFooter; 82 | -------------------------------------------------------------------------------- /components/home/ui/card-item.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Icon } from "@/components/ui/icon"; 4 | import type { ColorClasses, Page } from "@/config/home"; 5 | import { Link } from "@heroui/link"; 6 | import { clsx as cx } from "clsx"; 7 | 8 | interface CardItemProps { 9 | page: Page; 10 | classes: ColorClasses; 11 | } 12 | 13 | export const CardItem = ({ page, classes }: CardItemProps) => { 14 | return ( 15 | 23 |
24 |
30 | {page.icon && ( 31 | 35 | )} 36 |
37 | 38 | {page.title} 39 | 40 |
41 |
42 | {page.code && ( 43 | 50 | {page.code} 51 | 52 | )} 53 | 60 |
61 | 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /components/home/ui/card-section.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from "@/components/ui/icon"; 2 | import type { Section } from "@/config/home"; 3 | import { colorSchemes } from "@/config/home"; 4 | import { clsx as cx } from "clsx"; 5 | import { CardItem } from "./card-item"; 6 | 7 | interface CardSectionProps extends Section {} 8 | 9 | export const CardSection = ({ 10 | title, 11 | description, 12 | icon, 13 | color, 14 | pages, 15 | }: CardSectionProps) => { 16 | const classes = colorSchemes[color]; 17 | 18 | return ( 19 |
25 |
26 |
27 |
28 |
34 | 41 |
42 |
43 |

44 | {title} 45 |

46 |

47 | {description} 48 |

49 |
50 |
51 | 52 |
53 | {pages.map((page) => ( 54 | 55 | ))} 56 |
57 |
58 |
59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /components/layout/BaseLayout.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX, ReactNode } from "react"; 2 | 3 | interface BaseLayoutProps { 4 | children: ReactNode; 5 | } 6 | 7 | export const BaseLayout = ({ children }: BaseLayoutProps): JSX.Element => { 8 | return ( 9 |
10 |
{children}
11 |
12 | ); 13 | }; 14 | 15 | export default BaseLayout; 16 | -------------------------------------------------------------------------------- /components/layout/CFLayout.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import Footer from "../cf/Footer"; 3 | import { BaseLayout } from "./BaseLayout"; 4 | 5 | interface CFLayoutProps { 6 | children: ReactNode; 7 | } 8 | 9 | export const CFLayout = ({ children }: CFLayoutProps) => { 10 | return ( 11 | 12 |
13 |
14 |
15 |
{children}
16 |
17 |
18 | 19 |
20 |
21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | export default CFLayout; 28 | -------------------------------------------------------------------------------- /components/primitives.ts: -------------------------------------------------------------------------------- 1 | import { tv } from "tailwind-variants"; 2 | 3 | export const title = tv({ 4 | base: "tracking-tight inline font-semibold", 5 | variants: { 6 | color: { 7 | violet: "from-[#FF1CF7] to-[#b249f8]", 8 | yellow: "from-[#FF705B] to-[#FFB457]", 9 | blue: "from-[#5EA2EF] to-[#0072F5]", 10 | cyan: "from-[#00b7fa] to-[#01cfea]", 11 | green: "from-[#6FEE8D] to-[#17c964]", 12 | pink: "from-[#FF72E1] to-[#F54C7A]", 13 | foreground: "dark:from-[#FFFFFF] dark:to-[#4B4B4B]", 14 | }, 15 | size: { 16 | sm: "text-3xl lg:text-4xl", 17 | md: "text-[2.3rem] lg:text-5xl leading-9", 18 | lg: "text-4xl lg:text-6xl", 19 | }, 20 | fullWidth: { 21 | true: "w-full block", 22 | }, 23 | }, 24 | defaultVariants: { 25 | size: "md", 26 | }, 27 | compoundVariants: [ 28 | { 29 | color: [ 30 | "violet", 31 | "yellow", 32 | "blue", 33 | "cyan", 34 | "green", 35 | "pink", 36 | "foreground", 37 | ], 38 | class: "bg-clip-text text-transparent bg-gradient-to-b", 39 | }, 40 | ], 41 | }); 42 | 43 | export const subtitle = tv({ 44 | base: "w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full", 45 | variants: { 46 | fullWidth: { 47 | true: "!w-full", 48 | }, 49 | }, 50 | defaultVariants: { 51 | fullWidth: true, 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /components/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { HeroUIProvider } from "@heroui/react"; 4 | import type { ThemeProviderProps } from "next-themes"; 5 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 6 | import { useRouter } from "next/router"; 7 | import type { ReactNode } from "react"; 8 | // import { fontSans, fontMono } from '@/config/fonts'; 9 | 10 | export interface ProvidersProps { 11 | children: ReactNode; 12 | themeProps?: ThemeProviderProps; 13 | } 14 | 15 | export function Providers({ children, themeProps }: ProvidersProps) { 16 | const router = useRouter(); 17 | 18 | return ( 19 | router.push(path)}> 20 | 21 |
{children}
22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /components/theme-switch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Icon } from "@/components/ui/icon"; 4 | import { type SwitchProps, useSwitch } from "@heroui/switch"; 5 | import { useIsSSR } from "@react-aria/ssr"; 6 | import { VisuallyHidden } from "@react-aria/visually-hidden"; 7 | import { clsx as cx } from "clsx"; 8 | import { motion } from "framer-motion"; 9 | import { useTheme } from "next-themes"; 10 | import { useEffect, useState } from "react"; 11 | import type { FC } from "react"; 12 | 13 | export interface ThemeSwitchProps { 14 | className?: string; 15 | classNames?: SwitchProps["classNames"]; 16 | } 17 | 18 | export const ThemeSwitch: FC = ({ 19 | className, 20 | classNames, 21 | }) => { 22 | const { theme, setTheme, systemTheme } = useTheme(); 23 | const isSSR = useIsSSR(); 24 | const [mounted, setMounted] = useState(false); 25 | 26 | useEffect(() => { 27 | const initialTheme = theme || systemTheme || "light"; 28 | setTheme(initialTheme); 29 | setMounted(true); 30 | }, [theme, systemTheme, setTheme]); 31 | 32 | const currentTheme = theme || systemTheme; 33 | 34 | const onChange = () => { 35 | currentTheme === "light" ? setTheme("dark") : setTheme("light"); 36 | }; 37 | 38 | const { 39 | Component, 40 | slots, 41 | isSelected, 42 | getBaseProps, 43 | getInputProps, 44 | getWrapperProps, 45 | } = useSwitch({ 46 | isSelected: currentTheme === "light" || isSSR, 47 | "aria-label": `Switch to ${currentTheme === "light" || isSSR ? "dark" : "light"} mode`, 48 | onChange, 49 | }); 50 | 51 | // Prevent hydration mismatch 52 | if (!mounted || isSSR) { 53 | return null; 54 | } 55 | 56 | return ( 57 | 66 | 67 | 68 | 69 |
88 | 95 | {!isSelected || isSSR ? ( 96 |
97 | 98 |
99 | ☀️ 100 |
101 |
102 | ) : ( 103 |
104 | 105 |
106 | 🌙 107 |
108 |
109 | )} 110 |
111 |
112 |
113 | ); 114 | }; 115 | -------------------------------------------------------------------------------- /components/ui/color-scheme.tsx: -------------------------------------------------------------------------------- 1 | import type { ColorScheme } from "@/config/home"; 2 | import { clsx as cx } from "clsx"; 3 | import type { ReactNode } from "react"; 4 | 5 | interface ColorSchemeBoxProps { 6 | scheme: ColorScheme; 7 | children: ReactNode; 8 | className?: string; 9 | withBorder?: boolean; 10 | withBackground?: boolean; 11 | } 12 | 13 | const schemeClasses: Record< 14 | ColorScheme, 15 | { 16 | bg: string; 17 | border: string; 18 | text: string; 19 | gradient: string; 20 | iconBg: string; 21 | } 22 | > = { 23 | danger: { 24 | bg: "bg-red-50 dark:bg-red-900/30", 25 | border: "border-red-100 dark:border-red-900/30", 26 | text: "text-red-600 dark:text-red-400", 27 | gradient: "from-red-500 to-red-600", 28 | iconBg: "bg-red-500", 29 | }, 30 | warning: { 31 | bg: "bg-amber-50 dark:bg-amber-900/30", 32 | border: "border-amber-100 dark:border-amber-900/30", 33 | text: "text-amber-600 dark:text-amber-400", 34 | gradient: "from-amber-500 to-amber-600", 35 | iconBg: "bg-amber-500", 36 | }, 37 | primary: { 38 | bg: "bg-blue-50 dark:bg-blue-900/30", 39 | border: "border-blue-100 dark:border-blue-900/30", 40 | text: "text-blue-600 dark:text-blue-400", 41 | gradient: "from-blue-500 to-blue-600", 42 | iconBg: "bg-blue-500", 43 | }, 44 | }; 45 | 46 | export function ColorSchemeBox({ 47 | scheme, 48 | children, 49 | className, 50 | withBorder = false, 51 | withBackground = true, 52 | }: ColorSchemeBoxProps) { 53 | const classes = schemeClasses[scheme]; 54 | 55 | return ( 56 |
63 | {children} 64 |
65 | ); 66 | } 67 | 68 | export function ColorSchemeText({ 69 | scheme, 70 | children, 71 | className, 72 | }: Omit) { 73 | return ( 74 | 75 | {children} 76 | 77 | ); 78 | } 79 | 80 | export function getSchemeClasses(scheme: ColorScheme) { 81 | return schemeClasses[scheme]; 82 | } 83 | -------------------------------------------------------------------------------- /components/ui/icon.tsx: -------------------------------------------------------------------------------- 1 | import { type IconKey, icons } from "@/config/icons"; 2 | import { clsx as cx } from "clsx"; 3 | import type { LucideProps } from "lucide-react"; 4 | 5 | export interface IconProps extends LucideProps { 6 | name: IconKey; 7 | } 8 | 9 | export function Icon({ name, className, ...props }: IconProps) { 10 | const Icon = icons[name]; 11 | 12 | if (!Icon) { 13 | return null; 14 | } 15 | 16 | return ; 17 | } 18 | -------------------------------------------------------------------------------- /config/fonts.ts: -------------------------------------------------------------------------------- 1 | // import { Fira_Code as FontMono, Inter as FontSans } from "next/font/google"; 2 | 3 | // export const fontSans = FontSans({ 4 | // subsets: ["latin"], 5 | // variable: "--font-sans", 6 | // }); 7 | 8 | // export const fontMono = FontMono({ 9 | // subsets: ["latin"], 10 | // variable: "--font-mono", 11 | // }); 12 | -------------------------------------------------------------------------------- /config/home.ts: -------------------------------------------------------------------------------- 1 | import { 2 | blockPageTranslations, 3 | challengePageTranslations, 4 | errorPageTranslations, 5 | } from "./i18n"; 6 | import type { IconKey } from "./icons"; 7 | import { blockPages, challengePages, errorPages } from "./routes"; 8 | 9 | export type ColorScheme = "danger" | "warning" | "primary"; 10 | 11 | export interface ColorClasses { 12 | itemBg: string; 13 | iconBg: string; 14 | iconText: string; 15 | codeBg: string; 16 | codeText: string; 17 | border: string; 18 | } 19 | 20 | export interface Page { 21 | title: string; 22 | path: string; 23 | code?: string; 24 | icon?: IconKey; 25 | } 26 | 27 | export interface Section { 28 | title: string; 29 | description: string; 30 | icon: IconKey; 31 | color: ColorScheme; 32 | pages: Page[]; 33 | } 34 | 35 | export const colorSchemes: Record = { 36 | danger: { 37 | itemBg: "hover:bg-red-50/70 dark:hover:bg-red-900/20 hover:shadow-sm", 38 | iconBg: 39 | "bg-gradient-to-br from-red-300 to-red-500 dark:from-red-400 dark:to-red-600", 40 | iconText: "text-white", 41 | codeBg: "bg-red-50/80 dark:bg-red-900/40", 42 | codeText: "text-red-600 dark:text-red-400", 43 | border: "border-red-100 dark:border-red-900/30", 44 | }, 45 | warning: { 46 | itemBg: "hover:bg-amber-50/70 dark:hover:bg-amber-900/20 hover:shadow-sm", 47 | iconBg: 48 | "bg-gradient-to-br from-amber-300 to-amber-500 dark:from-amber-400 dark:to-amber-600", 49 | iconText: "text-white", 50 | codeBg: "bg-amber-50/80 dark:bg-amber-900/40", 51 | codeText: "text-amber-600 dark:text-amber-400", 52 | border: "border-amber-100 dark:border-amber-900/30", 53 | }, 54 | primary: { 55 | itemBg: "hover:bg-blue-50/70 dark:hover:bg-blue-900/20 hover:shadow-sm", 56 | iconBg: 57 | "bg-gradient-to-br from-blue-300 to-blue-500 dark:from-blue-400 dark:to-blue-600", 58 | iconText: "text-white", 59 | codeBg: "bg-blue-50/80 dark:bg-blue-900/40", 60 | codeText: "text-blue-600 dark:text-blue-400", 61 | border: "border-blue-100 dark:border-blue-900/30", 62 | }, 63 | }; 64 | 65 | export const sections: Section[] = [ 66 | { 67 | title: "Error Pages", 68 | description: "Server error pages", 69 | icon: "triangle-alert", 70 | color: "danger", 71 | pages: Object.entries(errorPages).map(([type, config]) => ({ 72 | title: errorPageTranslations[type].title, 73 | path: `/cf/error/${type}/`, 74 | code: config.code, 75 | icon: config.icon, 76 | })), 77 | }, 78 | { 79 | title: "Block Pages", 80 | description: "Access denied pages", 81 | icon: "lock", 82 | color: "warning", 83 | pages: Object.entries(blockPages).map(([type, config]) => ({ 84 | title: blockPageTranslations[type].title, 85 | path: `/cf/block/${type}/`, 86 | code: config.code, 87 | icon: config.icon, 88 | })), 89 | }, 90 | { 91 | title: "Challenge Pages", 92 | description: "Security verification challenges", 93 | icon: "shield-check", 94 | color: "primary", 95 | pages: Object.entries(challengePages).map(([type, config]) => ({ 96 | title: challengePageTranslations[type].title, 97 | path: `/cf/challenge/${type}/`, 98 | icon: config.icon, 99 | })), 100 | }, 101 | ]; 102 | -------------------------------------------------------------------------------- /config/i18n.ts: -------------------------------------------------------------------------------- 1 | export interface PageTranslation { 2 | title: string; 3 | message: string; 4 | } 5 | 6 | export interface InterfaceTranslations { 7 | message: string; 8 | } 9 | 10 | export interface BlockPageTranslation extends PageTranslation {} 11 | export interface ErrorPageTranslation extends PageTranslation {} 12 | export interface ChallengePageTranslation extends PageTranslation {} 13 | 14 | export const blockPageTranslations: Record = { 15 | ip: { 16 | title: "Your IP is blocked", 17 | message: "The owner of this website has banned your IP address.", 18 | }, 19 | waf: { 20 | title: "You're blocked by WAF", 21 | message: 22 | "The Cloudflare WAF (Web Application Firewall) has blocked your request.", 23 | }, 24 | "rate-limit": { 25 | title: "Rate Limit Block - 429", 26 | message: 27 | "You have made too many requests. Please wait a moment before trying again.", 28 | }, 29 | } as const; 30 | 31 | export const errorPageTranslations: Record = { 32 | "500s": { 33 | title: "Internal Server Error", 34 | message: 35 | "Please try again later, there was an unexpected error on the site.", 36 | }, 37 | "1000s": { 38 | title: "DNS Resolution Error", 39 | message: 40 | "The requested hostname could not be resolved. Don't worry, it's not your problem.", 41 | }, 42 | } as const; 43 | 44 | export const challengePageTranslations: Record< 45 | string, 46 | ChallengePageTranslation 47 | > = { 48 | interactive: { 49 | title: "Interactive Challenge", 50 | message: "Please complete this CAPTCHA to access the site.", 51 | }, 52 | managed: { 53 | title: "I'm Under Attack Mode™", 54 | message: "Complete CAPTCHA to proceed. This is a general security check.", 55 | }, 56 | country: { 57 | title: "Challenge", 58 | message: 59 | "Additional verification is required for visitors from your Country/Region.", 60 | }, 61 | javascript: { 62 | title: "Please wait...", 63 | message: 64 | "Please wait a moment while our security system verifies your request.", 65 | }, 66 | } as const; 67 | 68 | export const interfaceTranslations: Record = { 69 | "error-details": { 70 | message: "Learn more", 71 | }, 72 | "connection-tracking": { 73 | message: "Connection Tracking", 74 | }, 75 | "network-status-you": { 76 | message: "You", 77 | }, 78 | "network-status-cdn": { 79 | message: "CDN", 80 | }, 81 | "network-status-origin": { 82 | message: "Origin", 83 | }, 84 | } as const; 85 | -------------------------------------------------------------------------------- /config/icons.ts: -------------------------------------------------------------------------------- 1 | // 1. 在这里引入图标,注意是 `Copy Component Name` 按钮复制的名称 2 | import { 3 | Activity, 4 | ArrowRight, 5 | BadgeAlert, 6 | BookOpen, 7 | CheckCircle, 8 | ChevronsLeftRightEllipsis, 9 | Construction, 10 | FileQuestion, 11 | Github, 12 | Heart, 13 | HelpCircle, 14 | Info, 15 | Lightbulb, 16 | Loader, 17 | LoaderCircle, 18 | Lock, 19 | type LucideIcon, 20 | Moon, 21 | Network, 22 | Shield, 23 | ShieldAlert, 24 | ShieldBan, 25 | ShieldCheck, 26 | ShieldEllipsis, 27 | Sun, 28 | TriangleAlert, 29 | XCircle, 30 | } from "lucide-react"; 31 | 32 | // 2. 在这里添加你想要的图标到类型列表中 33 | export type IconKey = 34 | | "shield-ban" 35 | | "shield" 36 | | "loader" 37 | | "badge-alert" 38 | | "construction" 39 | | "shield-check" 40 | | "shield-alert" 41 | | "shield-ellipsis" 42 | | "file-question" 43 | | "sun" 44 | | "moon" 45 | | "triangle-alert" 46 | | "lock" 47 | | "info" 48 | | "book-open" 49 | | "github" 50 | | "heart" 51 | | "arrow-right" 52 | | "check-circle" 53 | | "x-circle" 54 | | "loader-circle" 55 | | "activity" 56 | | "network" 57 | | "help-circle" 58 | | "lightbulb" 59 | | "chevron-left-right-ellipsis"; 60 | 61 | // 3. 在这里添加你想要的图标到 `icons` 映射字典中 62 | export const icons: Record = { 63 | "shield-ban": ShieldBan, 64 | shield: Shield, 65 | loader: Loader, 66 | "badge-alert": BadgeAlert, 67 | construction: Construction, 68 | "shield-check": ShieldCheck, 69 | "shield-alert": ShieldAlert, 70 | "shield-ellipsis": ShieldEllipsis, 71 | "file-question": FileQuestion, 72 | sun: Sun, 73 | moon: Moon, 74 | "triangle-alert": TriangleAlert, 75 | lock: Lock, 76 | info: Info, 77 | "book-open": BookOpen, 78 | github: Github, 79 | heart: Heart, 80 | "arrow-right": ArrowRight, 81 | "check-circle": CheckCircle, 82 | "x-circle": XCircle, 83 | "loader-circle": LoaderCircle, 84 | activity: Activity, 85 | network: Network, 86 | "help-circle": HelpCircle, 87 | lightbulb: Lightbulb, 88 | "chevron-left-right-ellipsis": ChevronsLeftRightEllipsis, 89 | }; 90 | -------------------------------------------------------------------------------- /config/routes.ts: -------------------------------------------------------------------------------- 1 | import type { IconKey } from "@/config/icons"; 2 | 3 | interface BasePageConfig { 4 | type: string; 5 | code: string; 6 | icon: IconKey; 7 | networkStatus: NetworkStatusConfig; 8 | } 9 | 10 | export type BlockPageConfig = BasePageConfig & { 11 | type: "ip" | "waf" | "rate-limit"; 12 | }; 13 | 14 | export type ErrorPageConfig = BasePageConfig & { 15 | type: "500s" | "1000s"; 16 | box: string; 17 | }; 18 | 19 | export type ChallengePageConfig = BasePageConfig & { 20 | type: "interactive" | "managed" | "country" | "javascript"; 21 | box: string | null; 22 | }; 23 | 24 | export type PageType = "error" | "block" | "challenge"; 25 | 26 | type BlockType = BlockPageConfig["type"]; 27 | type ErrorType = ErrorPageConfig["type"]; 28 | type ChallengeType = ChallengePageConfig["type"]; 29 | 30 | export const directories: PageType[] = ["block", "error", "challenge"]; 31 | 32 | export const types = { 33 | block: ["ip", "waf", "rate-limit"] as BlockType[], 34 | error: ["500s", "1000s"] as ErrorType[], 35 | challenge: [ 36 | "interactive", 37 | "managed", 38 | "country", 39 | "javascript", 40 | ] as ChallengeType[], 41 | }; 42 | 43 | /** 44 | * Block page configurations 45 | * @type {Record} 46 | */ 47 | export const blockPages: Record = { 48 | ip: { 49 | type: "ip", 50 | code: "1006", 51 | icon: "shield-ban", 52 | networkStatus: { 53 | clientStatus: "error", 54 | edgeStatus: "success", 55 | }, 56 | }, 57 | waf: { 58 | type: "waf", 59 | code: "1010", 60 | icon: "shield", 61 | networkStatus: { 62 | clientStatus: "error", 63 | edgeStatus: "success", 64 | }, 65 | }, 66 | "rate-limit": { 67 | type: "rate-limit", 68 | code: "429", 69 | icon: "loader", 70 | networkStatus: { 71 | clientStatus: "challenging", 72 | edgeStatus: "success", 73 | }, 74 | }, 75 | }; 76 | 77 | /** 78 | * Error page configurations 79 | * @type {Record} 80 | */ 81 | export const errorPages: Record = { 82 | "500s": { 83 | type: "500s", 84 | code: "500", 85 | box: "CLOUDFLARE_ERROR_500S_BOX", 86 | icon: "badge-alert", 87 | networkStatus: { 88 | clientStatus: "success", 89 | edgeStatus: "success", 90 | originStatus: "error", 91 | }, 92 | }, 93 | "1000s": { 94 | type: "1000s", 95 | code: "1000", 96 | box: "CLOUDFLARE_ERROR_1000S_BOX", 97 | icon: "construction", 98 | networkStatus: { 99 | clientStatus: "success", 100 | edgeStatus: "success", 101 | originStatus: "error", 102 | }, 103 | }, 104 | }; 105 | 106 | /** 107 | * Challenge page configurations 108 | * @type {Record} 109 | */ 110 | export const challengePages: Record = { 111 | interactive: { 112 | type: "interactive", 113 | code: "403", 114 | box: "CAPTCHA_BOX", 115 | icon: "shield", 116 | networkStatus: { 117 | clientStatus: "challenging", 118 | edgeStatus: "success", 119 | }, 120 | }, 121 | managed: { 122 | type: "managed", 123 | code: "403", 124 | box: "CAPTCHA_BOX", 125 | icon: "shield-check", 126 | networkStatus: { 127 | clientStatus: "challenging", 128 | edgeStatus: "success", 129 | }, 130 | }, 131 | country: { 132 | type: "country", 133 | code: "403", 134 | box: "CAPTCHA_BOX", 135 | icon: "shield-alert", 136 | networkStatus: { 137 | clientStatus: "challenging", 138 | edgeStatus: "success", 139 | }, 140 | }, 141 | javascript: { 142 | type: "javascript", 143 | code: "403", 144 | box: "IM_UNDER_ATTACK_BOX", 145 | icon: "shield-ellipsis", 146 | networkStatus: { 147 | clientStatus: "challenging", 148 | edgeStatus: "success", 149 | }, 150 | }, 151 | }; 152 | 153 | export interface NetworkStatusConfig { 154 | clientStatus: "success" | "error" | "challenging"; 155 | edgeStatus: "success" | "error"; 156 | originStatus?: "success" | "error"; 157 | className?: string; 158 | } 159 | -------------------------------------------------------------------------------- /config/site.ts: -------------------------------------------------------------------------------- 1 | export type SiteConfig = typeof siteConfig; 2 | 3 | export const siteConfig = { 4 | name: "Cloudflare WAF Custom Pages", 5 | description: 6 | "A beautiful, out-of-the-box Cloudflare WAF custom page template.", 7 | headerNavItems: [ 8 | { 9 | label: "Home", 10 | href: "/", 11 | }, 12 | ], 13 | links: { 14 | // 设置为 "#" 则不会跳转到其他页面。 15 | github: "https://github.com/Alice39s/cloudflare-custom-pages-nextjs", 16 | docs: "https://github.com/Alice39s/cloudflare-custom-pages-nextjs?tab=readme-ov-file#-usage", 17 | }, 18 | // 是否输出版权信息, 生产环境建议关闭,如二次开发等情况则建议开启,感谢支持! 19 | enableCopyrightConsole: true, 20 | }; 21 | -------------------------------------------------------------------------------- /docs/assets/block-from-ip-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alice39s/cloudflare-custom-pages-nextjs/8ee575f83c1e70176c0ca95d0f40d050793cbcdf/docs/assets/block-from-ip-dark.png -------------------------------------------------------------------------------- /docs/assets/block-from-ip-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alice39s/cloudflare-custom-pages-nextjs/8ee575f83c1e70176c0ca95d0f40d050793cbcdf/docs/assets/block-from-ip-light.png -------------------------------------------------------------------------------- /docs/assets/captcha-ic-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alice39s/cloudflare-custom-pages-nextjs/8ee575f83c1e70176c0ca95d0f40d050793cbcdf/docs/assets/captcha-ic-dark.png -------------------------------------------------------------------------------- /docs/assets/captcha-ic-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alice39s/cloudflare-custom-pages-nextjs/8ee575f83c1e70176c0ca95d0f40d050793cbcdf/docs/assets/captcha-ic-light.png -------------------------------------------------------------------------------- /docs/assets/error-500s-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alice39s/cloudflare-custom-pages-nextjs/8ee575f83c1e70176c0ca95d0f40d050793cbcdf/docs/assets/error-500s-dark.png -------------------------------------------------------------------------------- /docs/assets/error-500s-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alice39s/cloudflare-custom-pages-nextjs/8ee575f83c1e70176c0ca95d0f40d050793cbcdf/docs/assets/error-500s-light.png -------------------------------------------------------------------------------- /docs/assets/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alice39s/cloudflare-custom-pages-nextjs/8ee575f83c1e70176c0ca95d0f40d050793cbcdf/docs/assets/home.png -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const baseConfig = { 3 | reactStrictMode: true, 4 | typescript: { 5 | // !! WARN !! 6 | // Dangerously allow production builds to successfully complete even if 7 | // your project has type errors. 8 | // !! WARN !! 9 | // see https://nextjs.org/docs/app/api-reference/config/next-config-js/typescript 10 | ignoreBuildErrors: true, 11 | }, 12 | 13 | compress: true, 14 | 15 | trailingSlash: true, 16 | }; 17 | 18 | const devConfig = { 19 | ...baseConfig, 20 | }; 21 | 22 | const prodConfig = { 23 | ...baseConfig, 24 | // 生产环境使用 export mode 25 | output: "export", 26 | productionBrowserSourceMaps: false, 27 | 28 | webpack: (config, { isServer }) => { 29 | if (!isServer) { 30 | // 优化分包策略 31 | config.optimization.splitChunks = { 32 | chunks: "all", 33 | minSize: 10000, // 降低最小体积阈值 34 | maxSize: 244000, 35 | minChunks: 1, 36 | maxAsyncRequests: 30, 37 | maxInitialRequests: 30, 38 | }; 39 | 40 | // 优化输出文件名格式 41 | config.output.chunkFilename = isServer 42 | ? "../static/chunks/[name].[contenthash].js" 43 | : "static/chunks/[name].[contenthash].js"; 44 | } 45 | 46 | return config; 47 | }, 48 | }; 49 | 50 | module.exports = process.env.NODE_ENV === "production" ? prodConfig : devConfig; 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudflare-custom-pages-nextjs", 3 | "version": "1.0.8", 4 | "license": "GPL-3.0-or-later", 5 | "description": "A beautiful, out-of-the-box Cloudflare WAF custom page template.", 6 | "author": { 7 | "name": "Alice39s" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/Alice39s/cloudflare-custom-pages-nextjs.git" 12 | }, 13 | "private": true, 14 | "scripts": { 15 | "predev": "bun run format-lint", 16 | "dev": "next dev --turbo", 17 | "prebuild": "bun run format-lint", 18 | "build": "next build", 19 | "postbuild": "bun scripts/assets-process.ts", 20 | "prestart": "bun run build", 21 | "start": "bun x serve out -p 3001", 22 | "format-lint": "bun run format && bun run lint", 23 | "format": "bunx biome format --fix --write pages config components scripts types utils tsconfig.json tailwind.config.js postcss.config.js package.json next.config.js", 24 | "lint": "bunx biome check --write pages config components scripts types utils tsconfig.json tailwind.config.js postcss.config.js package.json next.config.js" 25 | }, 26 | "dependencies": { 27 | "@heroui/button": "^2.2.21", 28 | "@heroui/code": "^2.2.16", 29 | "@heroui/input": "^2.4.21", 30 | "@heroui/kbd": "^2.2.17", 31 | "@heroui/link": "^2.2.18", 32 | "@heroui/listbox": "^2.3.20", 33 | "@heroui/navbar": "^2.2.19", 34 | "@heroui/react": "^2.7.10", 35 | "@heroui/snippet": "^2.2.22", 36 | "@heroui/switch": "^2.2.19", 37 | "@heroui/system": "^2.4.17", 38 | "@heroui/theme": "^2.4.17", 39 | "@react-aria/ssr": "^3.9.9", 40 | "@react-aria/visually-hidden": "^3.8.25", 41 | "@types/glob": "^8.1.0", 42 | "clsx": "^2.1.1", 43 | "glob": "^11.0.2", 44 | "intl-messageformat": "^10.7.16", 45 | "lucide-react": "0.475.0", 46 | "next": "^15.3.3", 47 | "next-themes": "^0.4.6", 48 | "react": "^19.1.0", 49 | "react-dom": "^19.1.0", 50 | "recharts": "^2.15.3", 51 | "usehooks-ts": "^3.1.1" 52 | }, 53 | "devDependencies": { 54 | "@biomejs/biome": "1.9.4", 55 | "@next/eslint-plugin-next": "15.0.4", 56 | "@react-types/shared": "3.25.0", 57 | "@types/node": "20.5.7", 58 | "@types/react": "^19.1.8", 59 | "@types/react-dom": "^19.1.6", 60 | "@typescript-eslint/eslint-plugin": "8.11.0", 61 | "@typescript-eslint/parser": "8.11.0", 62 | "autoprefixer": "10.4.19", 63 | "cheerio": "^1.1.0", 64 | "eslint": "^8.57.1", 65 | "eslint-config-next": "15.0.4", 66 | "eslint-config-prettier": "9.1.0", 67 | "eslint-plugin-import": "^2.31.0", 68 | "eslint-plugin-jsx-a11y": "^6.10.2", 69 | "eslint-plugin-node": "^11.1.0", 70 | "eslint-plugin-prettier": "5.2.1", 71 | "eslint-plugin-react": "^7.37.5", 72 | "eslint-plugin-react-hooks": "^4.6.2", 73 | "eslint-plugin-unused-imports": "4.1.4", 74 | "postcss": "8.4.49", 75 | "prettier": "3.3.3", 76 | "serve": "^14.2.4", 77 | "tailwind-variants": "0.1.20", 78 | "tailwindcss": "3.4.16", 79 | "typescript": "5.6.3" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorBox } from "@/components/cf/ErrorBox"; 2 | import { CFLayout } from "@/components/layout/CFLayout"; 3 | import { Providers } from "@/components/providers"; 4 | import type { ErrorPageConfig } from "@/config/routes"; 5 | import { Button } from "@heroui/button"; 6 | import { useRouter } from "next/router"; 7 | 8 | export default function Custom404() { 9 | const router = useRouter(); 10 | const config: ErrorPageConfig = { 11 | type: "1000s", 12 | code: "404", 13 | title: "Page Not Found", 14 | message: "The page you are looking for could not be found.", 15 | box: "RAY_ID", 16 | icon: "file-question", 17 | networkStatus: { 18 | clientStatus: "success", 19 | edgeStatus: "success", 20 | originStatus: "error", 21 | }, 22 | }; 23 | 24 | return ( 25 | 26 | 27 |
28 | 29 |
30 | 33 | 36 |
37 |
38 |
39 |
40 | ); 41 | } 42 | 43 | export const getStaticProps = () => { 44 | return { 45 | props: {}, 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css"; 2 | import { Providers } from "@/components/providers"; 3 | import { siteConfig } from "@/config/site"; 4 | import type { AppProps } from "next/app"; 5 | import Head from "next/head"; 6 | 7 | export default function App({ Component, pageProps }: AppProps) { 8 | return ( 9 | 10 | 11 | {siteConfig.name} 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { siteConfig } from "@/config/site"; 2 | import { Head, Html, Main, NextScript } from "next/document"; 3 | 4 | export default function Document() { 5 | return ( 6 | 7 | 8 | {/* */} 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /pages/cf/[directory]/[type].tsx: -------------------------------------------------------------------------------- 1 | import { 2 | PageWrapper, 3 | getStaticPaths as getStaticPathsHelper, 4 | getStaticProps as getStaticPropsHelper, 5 | } from "@/components/cf/ui/PageWrapper"; 6 | 7 | import { type PageType, directories, types } from "@/config/routes"; 8 | 9 | interface DynamicPageProps { 10 | pageType: PageType; 11 | } 12 | 13 | export default function DynamicPage({ pageType }: DynamicPageProps) { 14 | return ; 15 | } 16 | 17 | export async function getStaticPaths() { 18 | const paths = directories.flatMap((directory) => 19 | types[directory].map((type) => ({ 20 | params: { directory, type }, 21 | })), 22 | ); 23 | 24 | return { 25 | paths, 26 | fallback: false, 27 | }; 28 | } 29 | 30 | export const getStaticProps = ({ 31 | params, 32 | }: { params: { directory: PageType; type: string } }) => { 33 | const validDirectories: PageType[] = directories; 34 | if (!validDirectories.includes(params.directory)) { 35 | return { 36 | notFound: true, 37 | }; 38 | } 39 | 40 | const result = getStaticPropsHelper(params.directory, { type: params.type }); 41 | 42 | return { 43 | ...result, 44 | props: { 45 | // biome-ignore lint/suspicious/noExplicitAny: 46 | ...((result as any).props || {}), 47 | pageType: params.directory, 48 | }, 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Hero } from "@/components/home/Hero"; 2 | import HomeFooter from "@/components/home/HomeFooter"; 3 | import { CardSection } from "@/components/home/ui/card-section"; 4 | import { Providers } from "@/components/providers"; 5 | import { sections } from "@/config/home"; 6 | import { Divider } from "@heroui/divider"; 7 | 8 | export default function Home() { 9 | return ( 10 | 11 |
12 |
13 | 14 | 18 |
19 |
20 | {sections.map((section) => ( 21 |
29 | 30 |
31 | ))} 32 |
33 |
34 |
35 | 36 |
37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /cf/ 3 | Allow: / -------------------------------------------------------------------------------- /scripts/assets-process.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | import * as path from "node:path"; 3 | import * as cheerio from "cheerio"; 4 | import { 5 | blockPageTranslations, 6 | challengePageTranslations, 7 | errorPageTranslations, 8 | } from "../config/i18n"; 9 | 10 | function getAllHtmlFiles(dirPath: string): string[] { 11 | const files: string[] = []; 12 | 13 | const items = fs.readdirSync(dirPath); 14 | 15 | for (const item of items) { 16 | const fullPath = path.join(dirPath, item); 17 | const stat = fs.statSync(fullPath); 18 | 19 | if (stat.isDirectory()) { 20 | files.push(...getAllHtmlFiles(fullPath)); 21 | } else if (path.extname(fullPath) === ".html") { 22 | files.push(fullPath); 23 | } 24 | } 25 | 26 | return files; 27 | } 28 | 29 | function processHtmlFile(filePath: string): void { 30 | try { 31 | const html = fs.readFileSync(filePath, "utf-8"); 32 | const $ = cheerio.load(html); 33 | 34 | $('link[rel="preload"]').each((_, element) => { 35 | const $element = $(element); 36 | const as = $element.attr("as"); 37 | 38 | if (as === "style") { 39 | $element.attr("rel", "stylesheet"); 40 | $element.removeAttr("as"); 41 | } else if (as === "font") { 42 | $element.remove(); 43 | } 44 | }); 45 | 46 | updateTDK($, filePath); 47 | 48 | // 添加 Cloudflare meta 标签,仅对 out/cf/ 目录下的 HTML 文件处理 49 | if (filePath.includes(path.join("out", "cf"))) { 50 | addCloudflareMetaTags($); 51 | } 52 | 53 | // Move all script tags from head to bottom of body 54 | moveScriptsToBodyBottom($); 55 | 56 | fs.writeFileSync(filePath, $.html()); 57 | console.log(`Processed: ${filePath}`); 58 | } catch (error) { 59 | console.error(`Error processing ${filePath}:`, error); 60 | } 61 | } 62 | 63 | function updateTDK($: cheerio.CheerioAPI, filePath: string): void { 64 | const pathParts = filePath.split(path.sep); 65 | const cfIndex = pathParts.findIndex((part) => part === "cf"); 66 | 67 | if (cfIndex === -1 || cfIndex + 2 >= pathParts.length) { 68 | return; 69 | } 70 | 71 | const directory = pathParts[cfIndex + 1]; 72 | const type = pathParts[cfIndex + 2]; 73 | 74 | let pageTitle = ""; 75 | let pageDescription = ""; 76 | 77 | if (directory === "block" && type in blockPageTranslations) { 78 | pageTitle = blockPageTranslations[type].title; 79 | pageDescription = blockPageTranslations[type].message; 80 | } else if (directory === "error" && type in errorPageTranslations) { 81 | pageTitle = errorPageTranslations[type].title; 82 | pageDescription = errorPageTranslations[type].message; 83 | } else if (directory === "challenge" && type in challengePageTranslations) { 84 | pageTitle = challengePageTranslations[type].title; 85 | pageDescription = challengePageTranslations[type].message; 86 | } 87 | 88 | if (pageTitle) { 89 | $("title").text(`${pageTitle} - Cloudflare`); 90 | } 91 | 92 | if (pageDescription) { 93 | const descriptionMeta = $('meta[name="description"]'); 94 | if (descriptionMeta.length > 0) { 95 | descriptionMeta.attr("content", pageDescription); 96 | } else { 97 | $("head").append( 98 | ``, 99 | ); 100 | } 101 | } 102 | 103 | const keywordsMeta = $('meta[name="keywords"]'); 104 | if (keywordsMeta.length === 0) { 105 | $("head").append( 106 | '', 107 | ); 108 | } 109 | } 110 | 111 | /** 112 | * Add Cloudflare-specific meta tags to the top of head section in HTML files 113 | * - client-ip: ::CLIENT_IP:: 114 | * - ray-id: ::RAY_ID:: 115 | * - location-code: ::GEO:: 116 | * - build-date: Current build timestamp 117 | * - version: Package version from package.json 118 | */ 119 | function addCloudflareMetaTags($: cheerio.CheerioAPI): void { 120 | const packagePath = path.join(__dirname, "../package.json"); 121 | let version = "unknown"; 122 | 123 | try { 124 | const packageContent = fs.readFileSync(packagePath, "utf-8"); 125 | const packageJson = JSON.parse(packageContent); 126 | version = packageJson.version || "unknown"; 127 | } catch (error) { 128 | console.warn("Failed to read package.json version:", error); 129 | } 130 | 131 | const buildDate = new Date().toISOString(); 132 | 133 | $("head").prepend(` 134 | 135 | 136 | 137 | 138 | 139 | `); 140 | } 141 | 142 | /** 143 | * Move all script tags from head to the bottom of body 144 | * 145 | * Cloudflare will embed all style and script files in one HTML in the 146 | * final error page. However, this will break Next.js's script defer behavior. 147 | * 148 | * By putting all script tags to the bottom, we create a similar behavior to 149 | * defer with Cloudflare created inline scripts. 150 | */ 151 | function moveScriptsToBodyBottom($: cheerio.CheerioAPI): void { 152 | // Find all script tags in the head 153 | const headScripts = $("head script"); 154 | 155 | if (headScripts.length === 0) { 156 | return; 157 | } 158 | 159 | // Create an array to preserve script order 160 | const scriptElements: cheerio.Element[] = []; 161 | 162 | headScripts.each((_, element) => { 163 | scriptElements.push(element); 164 | }); 165 | 166 | // Remove scripts from head 167 | headScripts.remove(); 168 | 169 | // Append all scripts to the bottom of body (before the closing body tag) 170 | for (const script of scriptElements) { 171 | $("body").append(script); 172 | } 173 | } 174 | 175 | function main() { 176 | const outDir = "./out"; 177 | 178 | try { 179 | if (!fs.existsSync(outDir)) { 180 | console.error("Directory ./out does not exist"); 181 | return; 182 | } 183 | 184 | const htmlFiles = getAllHtmlFiles(outDir); 185 | 186 | for (const file of htmlFiles) { 187 | processHtmlFile(file); 188 | } 189 | 190 | console.log("All files processed successfully!"); 191 | } catch (error) { 192 | console.error("Error:", error); 193 | } 194 | } 195 | 196 | main(); 197 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --font-sans: "Inter", "Inter Fallback", "Helvetica Neue", "Helvetica", "Arial", 7 | sans-serif; 8 | --font-mono: "JetBrains Mono", "Fira Code", "Fira Mono", "SFMono-Regular", 9 | "Consolas", "Liberation Mono", ui-monospace, monospace; 10 | } 11 | 12 | html { 13 | font-family: var(--font-sans); 14 | } 15 | 16 | code, 17 | pre { 18 | font-family: var(--font-mono); 19 | } 20 | 21 | .font-sans { 22 | font-family: var(--font-sans); 23 | } 24 | 25 | .font-mono { 26 | font-family: var(--font-mono); 27 | } 28 | 29 | @keyframes flow { 30 | 0% { 31 | transform: translateX(-100%); 32 | opacity: 0; 33 | } 34 | 10% { 35 | opacity: 1; 36 | } 37 | 90% { 38 | opacity: 1; 39 | } 40 | 100% { 41 | transform: translateX(100%); 42 | opacity: 0; 43 | } 44 | } 45 | 46 | @keyframes network-loading { 47 | 0% { 48 | transform: translateX(-100%); 49 | opacity: 0; 50 | } 51 | 50% { 52 | opacity: 1; 53 | } 54 | 100% { 55 | transform: translateX(100%); 56 | opacity: 0; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import { heroui } from "@heroui/theme"; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | content: [ 6 | "./config/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 9 | "./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}", 10 | ], 11 | theme: { 12 | extend: { 13 | fontFamily: { 14 | sans: ["var(--font-sans)"], 15 | mono: ["var(--font-mono)"], 16 | }, 17 | keyframes: { 18 | "fade-in": { 19 | "0%": { opacity: "0" }, 20 | "100%": { opacity: "1" }, 21 | }, 22 | "slide-in-from-bottom": { 23 | "0%": { transform: "translateY(1rem)", opacity: "0" }, 24 | "100%": { transform: "translateY(0)", opacity: "1" }, 25 | }, 26 | flow: { 27 | "0%": { 28 | transform: "translateX(-100%)", 29 | opacity: "0", 30 | }, 31 | "10%": { 32 | opacity: "1", 33 | }, 34 | "90%": { 35 | opacity: "1", 36 | }, 37 | "100%": { 38 | transform: "translateX(100%)", 39 | opacity: "0", 40 | }, 41 | }, 42 | "network-loading": { 43 | "0%": { 44 | transform: "translateX(-100%)", 45 | opacity: "0", 46 | }, 47 | "50%": { 48 | opacity: "1", 49 | }, 50 | "100%": { 51 | transform: "translateX(100%)", 52 | opacity: "0", 53 | }, 54 | }, 55 | }, 56 | animation: { 57 | "fade-in": "fade-in 0.3s ease-out forwards", 58 | "slide-in-from-bottom-4": "slide-in-from-bottom 0.4s ease-out forwards", 59 | flow: "flow 3s cubic-bezier(0.4,0,0.6,1) infinite", 60 | "network-loading": 61 | "network-loading 2s cubic-bezier(0.4,0,0.6,1) infinite", 62 | }, 63 | }, 64 | }, 65 | darkMode: "class", 66 | plugins: [heroui()], 67 | }; 68 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": [ 27 | "**/*.js", 28 | "**/*.jsx", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/types/**/*.ts", 32 | "next-env.d.ts", 33 | "out/types/**/*.ts" 34 | ], 35 | "exclude": ["node_modules"] 36 | } 37 | -------------------------------------------------------------------------------- /types/index.ts: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export type IconSvgProps = SVGProps & { 4 | size?: number; 5 | }; 6 | -------------------------------------------------------------------------------- /utils/console.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { siteConfig } from "@/config/site"; 3 | import pkg from "@/package.json" assert { type: "json" }; 4 | 5 | const print = () => { 6 | console.log( 7 | `\n%c${pkg.name}%c v${pkg.version}`, 8 | `background: linear-gradient(to right, #ff66cc, #9370db, #66ccff); 9 | -webkit-background-clip: text; 10 | color: transparent; 11 | font-size: 1.2rem; 12 | font-weight: bold;`, 13 | "font-size: 2em; color: #666;", 14 | ); 15 | 16 | console.log( 17 | "%cGitHub ↗", 18 | "color: #66ccff; font-size: 1em; cursor: pointer;", 19 | siteConfig.links.github, 20 | ); 21 | }; 22 | 23 | export default print; 24 | --------------------------------------------------------------------------------