Contents
- 1 Introduction
- 2 Part II: Implementation Method Selection
- 3 Specific implementation steps
- 3.1 Plans that require VPS
- 3.2 Solution without VPS (Cloudflare Worker + R2)
- 4 Conclusion
1 Introduction
I didn’t intend to write this article originally. The reason I suddenly jumped in to write it is because I suddenly felt a little tired of the behavior of “manually changing the background picture of the blog regularly”.
I have been manually changing the background image of my blog regularly (the image is stored on cloudflare R2). Originally, changing the background image every 1-2 months is not a big deal, but as I have used more and more background images, I feel a little reluctant to completely abandon the ones I used before (after all, I carefully selected them), but I am also a little reluctant to manually change them to the background images I used before.
What to do? Why not just make a random background, so that it will be like the emperor flipping the cards when he goes to bed, and I don't have to worry about it. I just start making it when I think of it, and I can also write an article about it.
2 Part II: Implementation Method Selection
When building a random image API, you can use "whether a VPS is needed" as a criterion to choose a solution that suits your actual conditions:
1. VPS required (suitable for those who have VPS or use Cloudflare Tunnel)
If you already have a VPS (or use Cloudflare Tunnel You can choose one of the following two methods:
• Local Storage:The pictures are directly stored on the VPS, and the server returns a random picture through the API. It is suitable for small-scale picture libraries, with fast access speed, but it takes up VPS storage space.
• Cloud Storage:Images are stored in cloud storage such as Cloudflare R2, S3, OSS, etc., and VPS is responsible for API logic. Suitable for large-scale image libraries, reducing the burden on VPS, but access depends on external storage.
2. Solutions that do not require VPS (Cloudflare Worker + Cloud Storage)
If you don't have a VPS, or want full hosting on Cloudflare, you can choose:
• Cloudflare Worker + Cloud Storage (R2 / GitHub):Cloudflare Worker processes API requests, and the images are stored in Cloudflare R2, GitHub warehouse, etc., and the image URL is directly returned. No server is required, the maintenance cost is low, and it is suitable for managed services.
Note: In addition to Cloudflare Worker, there are other options. I am already using Cloudflare, so it is natural for me to choose the Worker solution. In addition, from the perspective of global access, the fact that Worker can run on Cloudflare's edge network around the world is also a great advantage.
3 Specific implementation steps
3.1 Plans that require VPS
3.1.1 Local storage method
This method should be the easiest to implement random image API solution: you only need to install it on your VPS Nginx or ApacheAs a WEB server software, it can be realized with simple settings (note that if you choose Nginx, you need to ensure that PHP parsing has been correctly configured (for docker deployment of Nginx + PHP environment, please refer to the article:Docker series single container nginx, single container php (one version) multi-site sharing; For Baota Panel deployment of Nginx + PHP environment, please refer to the article:Linux panel series based on Baota panel to deploy V free of charge in source code); If you choose Apache, make sure the "mod_php" module is installed):
3.1.1.1 Standard version (not distinguishing between PC and mobile)
Create a new WEB site, create a "pc_img" directory in the root directory of the site (put the prepared wallpaper images), and an index.php file, the content of which is as follows:
Based on the above, a simple random image API can be implemented (the role of the index.php code isRandomly select an image and return it). The final directory structure is as follows:
/var/www/html/ │── pc_img/ # stores wallpaper images │── index.php # API code
Assume that the domain name corresponding to the site is "image.tangwudi.com"So, visit"https://image.tangwudi.com
", will randomly return the pictures in the "pc_img" directory.
3.1.1.2 Advanced version (compatible with Mobile-Detect, distinguishing between PC and mobile terminals)
This method is a bit more advanced and mainly relies on a project on Github:https://github.com/serbanghita/Mobile-Detect, the project is a PHP device detection library, mainly used for Determine the visitor's device type(like Mobile phone, tablet, PC). It is based on User-Agent parsing, can be used for Mobile terminal adaptation,Dynamic content adjustment,Redirect It can help developers Accurately distinguish between PC, mobile phone and tablet, very suitable for Responsive design, content optimization, API adaptation And other scenarios, such as the usage in this article.
You still need to create a new WEB site, but this time, in addition to creating a "pc_img" directory (for the wallpaper images you see when accessing the PC), you also need to create a "mobile_img" directory (for the wallpaper images you see when accessing the mobile terminal) and a Mobile_Detect.php file in the root directory of the site. You can download this file using the following command:
wget https://raw.githubusercontent.com/serbanghita/Mobile-Detect/master/Mobile_Detect.php -O Mobile_Detect.php
An index.php file with the following content:
isMobile() && !MobileDetect->isTablet();
// 选择文件夹(PC / 移动端不同壁纸)folder = is_mobile ? "mobile_img" : "pc_img";
// 获取本地对应文件夹中的图片img_array = glob(__DIR__ . "/folder/*.{webp,gif,jpg,png}", GLOB_BRACE);
// 如果目录中没有图片,返回 404
if (!img_array) {
http_response_code(404);
die("No images found");
}
// 随机选择一张图片
random_img =img_array[array_rand(img_array)];
// 302 重定向到该图片
header("Location: " . str_replace(__DIR__, '',random_img));
exit;
?>
The final directory structure is as follows:
/var/www/html/ │── pc_img/ # stores PC wallpapers│── mobile_img/ # stores mobile wallpapers│── Mobile_Detect.php # device detection library│── index.php # API code
进阶版 的访问方式与普通版 相同,但会根据设备类型自动选择 pc_img/ 或 mobile_img/ 目录中的图片。
3.1.2 Cloud Storage Methods
3.1.2.1 Overview
This method is suitable for friends who want to use cloud storage (such as Cloudflare R2, AWS S3, Github warehouse, etc.) to store images while still using VPS as the API logic processing layer. Compared with local storage solutions, this method reduces the storage pressure of VPS and can take advantage of the high availability and CDN acceleration capabilities of cloud storage.
3.1.2.2 Standard version (not distinguishing between PC and mobile)
Implementation steps:
- Store images in cloud storage (Cloudflare R2 / S3 / Github)and make sure they are accessible via HTTP.
- Create the pc_img.txt file on the VPS, stores links to images, with one image URL per line.
- VPS 端 PHP 读取 pc_img.txt 并随机返回一张图片的 URL.
- (Optional) Use a script to periodically update pc_img.txt, ensuring that new images in cloud storage can be accessed by the API.
Step 1: Store the image in cloud storage
by Cloudflare R2 For example, suppose your R2 public storage address is "https://image.tangwudi.com/
"(You need to configure R2 in advance, create a bucket, and perform other initialization operations. For details, please refer to the article:Home Data Center Series CloudFlare Tutorial (VIII) Introduction to CF R2 Functions and Detailed Tutorial on Building an Image Hosting Platform Based on R2), and the access address of each picture is as follows:
https://image.tangwudi.com/img1.jpg https://image.tangwudi.com/img2.png https://image.tangwudi.com/img3.webp
Step 2: Create pc_img.txt on the VPS
In pc_img.txt, each line contains the URL of a picture:
https://image.tangwudi.com/img1.jpg https://image.tangwudi.com/img2.png https://image.tangwudi.com/img3.webp
Step 3: VPS side index.php code
Code logic:
• Read the image list in pc_img.txt.
• If pc_img.txt is empty, return 404.
• Randomly select a picture and 302 Redirect The URL to the image.
access"https://image.tangwudi.com/
", will randomly return a picture URL in pc_img.txt
Directory structure:
/var/www/html/ │── pc_img.txt # stores the URL of the cloud storage image │── index.php # API code
Step 4: Update pc_img.txt regularly (optional)
If your cloud storage images are updated frequently, you can use a scheduled task Automatically sync img.txt.
Linux cron task example (for AWS S3)
Run the following command on the VPS to automatically write the image URL of the bucket to img.txt:
aws s3 ls s3://your-bucket-name/ --recursive | awk '{print "https://image.tangwudi.com/"$NF}' > /var/www/html/pc_img.txt
Then add a scheduled task in crontab -e and update it every hour:
0 * * * * /path/to/script.sh
If you are using Cloudflare R2, you can write a Python script to periodically call the r2 API to get the image list, and then update img.txt.
3.1.2.3 Advanced version (compatible with Mobile-Detect, distinguishing between PC and mobile terminals)
Compared with the normal version, the advanced version has an additional mobile_img.txt, which is used to store the URL of wallpaper images suitable for mobile devices. At the same time, like the advanced version of the local storage method, it also has an additional Mobile_Detect.php, and the content of index.php is also different:
isMobile() && !MobileDetect->isTablet();
// 选择文件(PC / 移动端不同壁纸)filename = is_mobile ? "mobile_img.txt" : "pc_img.txt";
// 文件不存在,返回 404
if (!file_exists(filename)) {
http_response_code(404);
die("File not found");
}
// 读取图片链接(跳过空行和注释)
pics = array_filter(file(filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES), function(line) {
return substr(line, 0, 1) !== '#';
});
// 如果图片列表为空,返回 404
if (empty(pics)) {
http_response_code(404);
die("No images found");
}
// 随机选择一张图片random_img = pics[array_rand(pics)];
// 302 重定向到该图片
header("Location: " . $random_img);
exit;
?>
The directory structure is as follows:
/var/www/html/ │── pc_img.txt # stores PC wallpaper URL │── mobile_img.txt # stores mobile wallpaper URL │── Mobile_Detect.php # device detection library │── index.php # API code
The access method of the advanced version is the same as the normal version, but it will automatically select the image URL in "pc_img.txt" or "mobile_img.txt" according to the device type.
use VPS + PHP To implement the random image API, you need to rely on index.php to handle each request: every time a user requests an image, the request must be sent back to the VPS, which will bring the following problems:
1. VPS connectivity requirements are high
• Back-to-source dependency: Each request must be made by the VPS back to the source to obtain the image. If there is a problem with the VPS (such as network failure, excessive server pressure, etc.), the request will fail or be delayed. In addition, who can guarantee that all users can quickly access the VPS?
• Bandwidth bottleneck:If the traffic is large and the images are large, the bandwidth of the VPS will become a bottleneck, especially when local storage is used and the CDN is not properly configured.
2. High performance pressure under high concurrency
• Each request will be carried by the VPS, which will cause the container to overload the VPS performance when the traffic is heavy.
• When the traffic suddenly increases, the VPS hardware and bandwidth may not be sufficient, resulting in Response timeout or collapse.
3. Cache Issues
• Even if you use CDN to cache images, Random image selection It still needs to be processed through the PHP backend. CDN will only cache the final returned image URL, but will not cache the judgment logic of index.php.
• Every time the image changes,Cache refresh It needs to be managed by going back to the VPS. This will result in a low cache hit rate and affect performance for large-scale updates or high-concurrency requests.
4. VPS Cost
• VPS will face increased costs if more bandwidth, processors, and memory are required in high-concurrency situations.
Therefore, for scenarios with low traffic and low requirements for high availability (such as the background image of a personal blog), the VPS-based random image API solution is OK. However, once the traffic is large or the requirements for high availability or response speed are high, this solution is not suitable.
Note 1: In addition to being familiar with conventional website building methods, the above VPS-based solutions also require you to be familiar with reverse proxy configuration (public IP website building method) or have a Cloudflare account and be able to complete the configuration of conventional functions (Cloudflare tunnel website building method). Friends who are not familiar with reverse proxy configuration can refer to the article:Linux panel series configure reverse proxy and use non-443 port for publishing(Use Pagoda Panel to implement reverse proxy),Docker series uses Docker to build its own reverse proxy based on NPM(Use NPM to implement reverse proxy); Friends who are not familiar with Cloudflare tunnel configuration can refer to the article:The home data center series uses tunnel technology to allow home broadband without public IP to use cloudflare for free to quickly build a website (recommended).
Note 2: The above content is a summary after I read the regular VPS-based random image API tutorials on the Internet. It is not the focus of this article, and I do not recommend it, so I will just skip it.
3.2 Solution without VPS (Cloudflare Worker + R2)
3.2.1 Overview
This solution is actually an improved version of the previous "VPS + cloud storage" solution. Although it does not require a VPS, it does require technology to replace the role of "index.php" in the "VPS + cloud storage" solution (randomly returning a URL for an image). My image hosting service was originally built using Cloudflare R2, so it is reasonable to use Cloudlfare Worker to implement the function of "index.php":


Earlier in the article, we mentioned some problems that the solution based on VPS + PHP to implement the random image API will face. However, the solution based on Cloudflare Worker does not have these problems. Instead, it has many advantages in these aspects:
1. Decentralization, no need to return to the source
• Workers run entirely on Edge Node, able to respond to requests directly without relying on a remote VPS.
• Pictures are stored in Cloudflare R2 or other cloud storage services, Worker will directly get the images from these places and return them without going back to the VPS.
2. Scalability under high concurrency
• Worker is Stateless It does not rely on server hardware resources. Cloudflare's edge network automatically Load Balancing, even if the number of requests increases dramatically, the performance will not be affected.
• Worker Press Request volume billingIn most cases, it is more cost-effective than VPS, especially for application scenarios with large traffic and small computing workloads (the free quota of 100,000 requests per day is definitely not enough for a regular personal blog as long as there is no DDoS attack).
3. Excellent caching mechanism
• Cloudflare itself provides Powerful caching mechanism, you can cache images and return results at the CDN node layer, reducing the computing pressure and back-to-source requirements of Workers.
• Images can be cached for longer periods of time without having to request them back every time.Significantly improved performance.
4. Flexible image storage and optimization
• Images can be stored in R2 or other cloud storage, and use Cloudflare's Polish and Mirage features Automatic optimization and compression (requires pro subscription) without having to do it manually.
• Workers can cooperate with cache strategies to perform intelligent caching and image updates to reduce duplicate requests.
5. Low cost and high availability
• The billing model for Workers is based on Request volume and resource consumption Billing is suitable for application scenarios with irregular traffic.
• unnecessary High performance VPS or load balancer To handle large traffic, reducing operation and maintenance costs.
The conclusion is: free is better, so why do you need a bicycle?
However, unlike the "VPS + cloud storage" scenario, the files on R2 cannot be edited directly. If you still follow the idea of img.txt in the "VPS + cloud storage" scenario, it will be very troublesome: every time you add a new picture, you need to re-edit and upload the pc_img.txt file (v2.0 version also needs to edit and upload the mobile_img.txt file). Although it can be made less troublesome through rclone or alist, it is still too torturous for a lazy person like me, so I need to change my thinking:
- Considering that the functions of the Worker method are all completed by code, the terminal type detection does not need to be implemented using a separate "Mobile_Detect.php" file like the VPS method. Therefore, the v1.0 version of the "Cloudflare Worker + R2" solution should include the terminal detection function (correspondingly, the corresponding image folders for the v1.0 version also require 2)
- The ideal management and operation effect is to adopt the folder management method: create directories "pc_img" and "mobile_img" on R2, and upload wallpapers suitable for PC and mobile terminals to the two directories respectively. Then Worker will automatically randomly select pictures from the two directories based on the access terminal device type, and return the picture URL to the visitor. No additional operations (txt files, etc.) are required.
In fact, the VPS + local storage method introduced earlier in the article also adopts a folder-based management and operation method. However, that is because PHP can directly obtain the file contents under the pc_img and mobile_img directories (through the local file system), and Cloudflare Worker can directly obtain the file contents in the R2 bucket (it is necessary to bind the R2 bucket in the Worker "Settings"), so this method can be used.
Note 1: If the cloud storage is not R2 (for example, a Github repository), then it will be quite troublesome to use this "folder method" to manage and maintain it, because the Worker can only obtain the image content in the Github repository through API calls. Although it seems to be just one more step, the complexity of the code and the debugging of the code will increase the difficulty, which is far less convenient than "Worker + R2". Of course, you can also take a step back and use the txt file method, which requires more effort and is a bit low-end.
Note 2: Cloudflare Worker is a serverless computing platform provided by Cloudflare that allows developers to run JavaScript code on Cloudflare's global edge network. It is based on the V8 engine, supports fast processing of HTTP requests, does not require server management, and is suitable for scenarios such as API proxy, content rewriting, and static resource distribution. Since Worker runs on Cloudflare's edge nodes, it can provide low-latency, high-availability services, and can be combined with KV storage, Durable Objects and other functions to expand application capabilities. For more information about Cloudflare Worker and configuration details, please refer to the article:Home Data Center Series Cloudflare Tutorial (VII) Introduction to CF Worker Functions and Practical Operation, Verification and Research on Related Technical Principles of Implementing "Beggar Version APO for WordPress" Function to Accelerate Website Access Based on Worker.
3.2.2 Preparation
3.2.2.1 Create a bucket on R2 specifically for random wallpapers (optional but highly recommended)
In fact, I personally recommend that you create a storage bucket specifically for random wallpapers, even if you have already used R2 as a photo hosting and have a ready-made storage bucket. In fact, it is not necessary to do this, but in addition to setting up domain-based routing when creating a new Worker (generally, the custom domain name of the storage bucket is used, and it is more convenient to set up a new storage bucket + a new domain name), for the Worker itself to work properly, it needs to correctly configure page rules, cache rules, WAF custom rules, etc., which have a higher priority than the Worker in the traffic sequence (for the relevant concepts of Cloudflare traffic sequence, please refer to the article:Cloudflare tutorial series for home data centers (Part 2) Introduction to the functions of each technical node in the CF overall solution traffic sequence), if you use the existing storage bucket directly, I am worried that friends who are not very familiar with Cloudflare traffic priority and these rule configurations may be negligent, resulting in the inability to achieve the random image effect. Therefore, the best way is to create a new storage bucket specifically for the random wallpaper function and use a new third-level domain name. This can largely avoid the impact of the previous rule configuration. For example, I created a new storage bucket named wallpaper.
Follow the picture tutorial below:



Select the Settings tab:

To configure a custom domain name:


Then click "Connect Domain" to generate a record of type R2 in DNS:

3.2.2.2 Create KV
Note: If you do not need the rate limiting function (to prevent malicious traffic) of subsequent v2.0 and v3.0 versions, you can skip this step.


Finish:

The reason why KV is chosen instead of D1 database for storage is that if it is only for temporary storage of IP access counts (cleared and restarted after 60 seconds), KV is sufficient and fast. If there are more complex requirements, such as recording the number of IP accesses and providing query functions, D1 database is required.
3.2.2.3 Upload wallpaper images to R2's wallpaper bucket
• PC wallpapers are stored in the pc_img/ directory
• Mobile wallpapers are stored in the mobile_img/ directory
The example directory structure is as follows:
pc_img/ │── pc1.jpg │── pc2.png │── pc3.webp mobile_img/ │── mobile1.jpg │── mobile2.png │── mobile3.webp
Note: The names of the pictures above are just for demonstration. In fact, you can use any name you want and it will not affect the effect.
3.2.3 Implementation of v1.0 worker code
3.2.3.1 Create a worker



3.2.3.2 Editing Code

Copy and paste the following v1.0 code into the worker and deploy it:
export default { async fetch(request, env, ctx) { const bucket = env.WALLPAPER_BUCKET; // Bind Cloudflare R2 bucket const url = new URL(request.url); const typeParam = url.searchParams.get("type"); // Get URL parameter?type=pc or?type=mobile const userAgent = request.headers.get('User-Agent') || ''; // Get the visitor's User-Agent information // Determine whether it is a PC device const isPC = /Windows NT|Macintosh|X11|Linux/i.test(userAgent); // Determine whether it is a mobile device const isMobile = /Android|iPhone|iPad|iPod|webOS|BlackBerry|IEMobile|Opera Mini/i.test(userAgent); // Get wallpaper from pc_img folder by default let folder = "pc_img/"; // If URL If the parameter specifies type=mobile, use mobile_img if (typeParam === "mobile") folder = "mobile_img/"; // If the URL parameter specifies type=pc, force the use of pc_img else if (typeParam === "pc") folder = "pc_img/"; // If there is no URL parameter, determine the device type based on User-Agent else if (!isPC && isMobile) folder = "mobile_img/"; try { // Get the image list of the corresponding folder in the R2 bucket const objects = await bucket.list({ prefix: folder }); const items = objects.objects; // If there are no images in the directory, return 404 if (items.length === 0) return new Response('No images found', { status: 404 }); // Randomly select an image const randomItem = items[Math.floor(Math.random() * items.length)]; // Generate the URL of the image const imageUrl = `https://wallpaper.tangwudi.com/${randomItem.key}`; // 302 redirect to the image URL return Response.redirect(imageUrl, 302); } catch (error) { console.error('Error:', error); // Record error information return new Response('Internal Server Error', { status: 500 }); // Return 500 when an error occurs } } };
As shown below:

The main function of this code is"Returns a random image URL from the Cloudflare R2 bucket based on the access device type (PC or mobile)", its workflow is as follows:
1️. Identify user device
• The code first checks the visitor's User-Agent(i.e. the browser's identity information).
• in the case of Mobile phone or tablet(iPhone, Android, etc.), select the wallpaper from the mobile_img/ folder.
• Otherwise, the wallpaper is picked up from the pc_img/ folder by default.
2️. Get a list of files
• Code to Cloudflare R2 Bucket Send a request to get a list of all images in the corresponding folder.
• If it is found that there are no pictures in the folder, it returns 404 Not Found (which means that there are no pictures available in the current category).
3️. Randomly select a picture
• The code will be selected from the image listRandom SelectionOne.
• Then try to get the specific content of this image from the R2 bucket.
• If the image cannot be found, return 404 Not Found (maybe the file is missing).
4️. Set the correct image format
• The code checks whether the R2 bucket has a record of this image. Content-Type(For example, image/png or image/jpeg).
• If not found, it defaults to treating it as image/jpeg to ensure the browser can load it correctly.
5️. Back to image 🚀
• The code finally returns the URL of the image to the user, so that the user can see the wallpaper normally.
3.2.3.3 Binding R2 Bucket


Fill in the variable name as follows, be careful not to make mistakes:
WALLPAPER_BUCKET
As shown below:

3.2.3.4 Adding an entry route



After final completion:

3.2.3.5 Final result
If each of the above configurations is correct, and there are no errors in the page rules, cache rules, and WAF rules (if the custom domain name of the bucket is a newly created domain name, there is generally no problem), and the access request can hit the worker, the normal effect is:
• Visit https://wallpaper.tangwudi.com
, according to the access terminal type, returns a The URL of the corresponding random wallpaper.
• Visit https://wallpaper.tangwudi.com?type=mobile
, specifically return Mobile wallpaper URL.
• Visit https://wallpaper.tangwudi.com?type=pc
, forced to return PC wallpaper URL.
I was happy for a while after the test was successful, but after careful consideration, I felt that this code only implemented the most basic random image API function. Although it may be enough for ordinary friends, if you really want to explore it in depth, you will find many problems. It feels like a semi-finished product.
3.2.3.6 Problem List
1. Not considering blocking crawlers
Although custom rules can be used in Cloudflare WAF to filter crawlers, I believe that most of my friends may not be familiar with how to configure it (although I strongly recommend that you study the custom rules of Cloudflare WAF, which is a free and powerful feature), so it is best to simply protect it in the code.
2. Not considering rate limits
When adding a route in the previous article, there is an option called "Number of failures", which is actually the processing behavior when the number of free uses of the worker exceeds 100,000 times per day. There are "Automatically close when failed (block)" and "Automatically open when failed (continue)". In the screenshot of the previous article, I mentioned that for version v1.0, it makes no difference to choose any behavior. Why do I say that?
Because for general websites, "Automatically open (continue) when failed" is equivalent to directly returning to the source, and the data can be accessed, but the speed may be slower because there is no cache. But for random image APIs,https://wallpaper.tangwudi.com
The response of "is entirely dependent on the worker. Once the limit is exceeded, the worker will stop working. At this time, if it returns directly to the source, R2 will not respond either. Therefore, it does not make sense to choose any option.
Note 1: Regarding the issue of exceeding the limit, some friends may say: "How can it be possible to exceed the limit? The daily quota is 100,000 requests. Generally speaking, even a personal blog with 1,000 visits per day only has 1,000. How can it be used up?" Generally speaking, this is true, but that is when there is no malicious traffic increase. I did not set any access restrictions on my image storage before, but one day in the early morning, 5T of traffic was increased within 2 hours, forcing me to directly close the direct access to the image storage. Having said so much, in fact, it is necessary to consider rate limiting in the code: if the number of visits to a certain IP exceeds the threshold within a unit of time, it will no longer respond.
Note 2: To be honest, the rate limit of the v2.0 code can only prevent some malicious traffic-swiping visits, but it will only be a second late in killing a real DDoS attack. Therefore, it is still necessary to reasonably configure Cloudflare's DDoS attack protection related functions.
3. Not considering cache optimization
Although it feels cool to change the background image every time you refresh or switch pages, it may not be a good thing from the perspective of the webmaster: each refresh means that the worker will process a request, and the browser frequently accesses the worker in a short period of time, which will increase unnecessary resource consumption. From the user experience, it may not be pleasant to have a background image loading process every time, so it is best to add a moderate browser local cache in the code (in fact, this can also be achieved through page rules or cache rules, but the same principle as WAF custom rules, I believe most friends may not set it).
4. The worker routing settings caused by API calls based on query parameters are too rough
The API options provided by v1.0 are "?type=pc" and "?type=mobile". However, since the worker entry route cannot have query parameters, in order to support the "?type=" option, its route can only use"wallpaper.tangwudi.com/* ", but in this case, the worker will have to handle requests from all paths (including malicious requests).
Note: You can certainly use WAF's custom rules to intercept here, but again, I believe most of my friends may not set it up, so it is best to achieve a certain degree of prevention at the worker code level.
5. Other issues
In addition, the v1.0 script is relatively simple, does not include error logging and debugging support, and is not complete from a code perspective.
Based on the above problems, version v2.0 was born.
3.2.4 Implementation of v2.0 worker code
Replace the previous v1.0 version code with the following v2.0 version code in the worker and deploy it:
export default { async fetch(request, env, ctx) { const bucket = env.WALLPAPER_BUCKET; // Bind Cloudflare R2 bucket const url = new URL(request.url); const pathname = url.pathname.toLowerCase(); // Get request path (ignore case) // **Block common crawler tools** const userAgent = (request.headers.get("User-Agent") || "").toLowerCase(); const cfRay = request.headers.get("cf-ray") || "unknown"; const botPatterns = [ "curl", "wget", "httpie", "python-requests", "scrapy", "postmanruntime", "go-http-client", "java/", "libwww-perl", "okhttp", "python-urllib", "apache-httpclient", "httpclient", "lwp::simple", "mechanize", "aiohttp", "axios", "reqwest", "puppeteer", "headlesschrome", "phantomjs" ]; if (!userAgent.trim()) { console.log(`[BLOCKED] Empty User-Agent - cf-ray: {cfRay}`); return new Response("Forbidden: Empty User-Agent", { status: 403 }); } if (botPatterns.some(bot => userAgent.includes(bot))) { console.log(`[BLOCKED] Bot Detected:{userAgent} - cf-ray: {cfRay}`); return new Response("Forbidden: Bot Detected", { status: 403 }); } // **Determine the folder (PC / Mobile)** let folder = "pc_img/"; // Default PC if (pathname.endsWith("fallback_mobile.jpg")) { folder = "mobile_img/"; // Force mobile wallpaper } else if (pathname.endsWith("fallback_pc.jpg")) { folder = "pc_img/"; // Force PC wallpaper } else { // **`fallback.jpg` needs to be identified based on User-Agent** const isMobile = /iphone|ipod|android|blackberry|iemobile|opera mini/.test(userAgent); folder = isMobile ? "mobile_img/" : "pc_img/"; } console.log("Requested Path:", pathname); console.log("Selected Folder:", folder); // **Initialize memory cache (Map)** const rateLimitCache = new Map(); // Used to cache rate limit data // **Rate limit (up to 10 requests within 60 seconds)** const clientIP = request.headers.get("CF-Connecting-IP") || "unknown"; const rateLimitKey = `rate-limit-{clientIP}`; // Try to get rate limit data from memory cache let currentCount = rateLimitCache.get(rateLimitKey); if (!currentCount) { // If not in cache, try to get from KV storage try { currentCount = await env.RATE_LIMIT_KV.get(rateLimitKey); if (currentCount) { currentCount = parseInt(currentCount); } else { currentCount = 0; } } catch (error) { console.error("Error fetching rate limit from KV:", error); // If KV read fails, default to 0 currentCount = 0; } } const newCount = currentCount + 1; // If rate limit exceeded, return 429 error if (newCount > 10) { return new Response("Too Many Requests", { status: 429 }); } // Update memory cache and KV storage rateLimitCache.set(rateLimitKey, newCount); try { // If KV If the storage exists, update the data in the KV storage await env.RATE_LIMIT_KV.put(rateLimitKey, newCount, { expirationTtl: 60 }); } catch (error) { console.error("Error updating rate limit in KV:", error); // Even if the KV update fails, it does not affect the request to continue processing } try { // **Get the list of files in the R2 bucket** const objects = await bucket.list({ prefix: folder }); const items = objects.objects; if (items.length === 0) { return new Response("No images found", { status: 404 }); } // **Randomly select a picture** const randomItem = items[Math.floor(Math.random() * items.length)]; if (!randomItem) { return new Response("No valid images", { status: 404 }); } const imageUrl = `https://wallpaper.tangwudi.com/${randomItem.key}`; // **Set cache policy** const headers = new Headers(); headers.set("Cache-Control", "public, max-age=600"); // Browser cache for 10 minutes headers.set("CDN-Cache-Control", "public, max-age=604800"); // Cloudflare CDN cache for 7 days headers.set("ETag", randomItem.etag); // **302 redirect to image URL** return Response.redirect(imageUrl, 302); } catch (error) { console.error("Error in Worker:", error); return new Response("Internal Server Error", { status: 500 }); } } };
Then you need to bind the KV space created previously (KV is needed to store the statistics of the number of IP visits):

Fill in the variable name as follows, be careful not to make a mistake:
RATE_LIMIT_KV
As shown below:

After the final completion, the bound resources include the R2 bucket and the KV namespace:

Compared with the above v1.0 code, this v2.0 code has made many important improvements and enhancements:
1. Bot Detection
v1.0 version codeIf you do not restrict crawlers and simply process requests directly, you may encounter a large amount of crawler traffic or malicious traffic brushing.v2.0 version codeA certain degree of protection is available:
// **Block common crawler tools** const userAgent = (request.headers.get("User-Agent") || "").toLowerCase(); const cfRay = request.headers.get("cf-ray") || "unknown"; const botPatterns = [ "curl", "wget", "httpie", "python-requests", "scrapy", "postmanruntime", "go-http-client", "java/", "libwww-perl", "okhttp", "python-urllib", "apache-httpclient", "httpclient", "lwp::simple", "mechanize", "aiohttp", "axios", "reqwest", "puppeteer", "headlesschrome", "phantomjs" ]; if (!userAgent.trim()) { console.log(`[BLOCKED] Empty User-Agent - cf-ray: {cfRay}`); return new Response("Forbidden: Empty User-Agent", { status: 403 }); } if (botPatterns.some(bot => userAgent.includes(bot))) { console.log(`[BLOCKED] Bot Detected:{userAgent} - cf-ray: ${cfRay}`); return new Response("Forbidden: Bot Detected", { status: 403 }); }
• Problem Solving:By matching the User-Agent field, determine whether it is a common crawler tool. If it is a crawler, return 403 forbidden access, which can effectively prevent unwanted automated access.
2. Rate Limiting
v1.0 version codeIn the , requests from any IP are not restricted, and may encounter abuse requests.v2.0 version codeMore protection capabilities:
// **Initialize memory cache (Map)** const rateLimitCache = new Map(); // Used to cache rate limit data // **Rate limit (maximum 10 requests in 60 seconds)** const clientIP = request.headers.get("CF-Connecting-IP") || "unknown"; const rateLimitKey = `rate-limit-${clientIP}`; // Try to get rate limit data from memory cache let currentCount = rateLimitCache.get(rateLimitKey); if (!currentCount) { // If not in cache, try to get from KV storage try { currentCount = await env.RATE_LIMIT_KV.get(rateLimitKey); if (currentCount) { currentCount = parseInt(currentCount); } else { currentCount = 0; } } catch (error) { console.error("Error fetching rate limit from KV:", error); // If KV read fails, default to starting from 0 currentCount = 0; } } const newCount = currentCount + 1; // If the rate limit is exceeded, return a 429 error if (newCount > 10) { return new Response("Too Many Requests", { status: 429 }); } // Update the memory cache and KV store rateLimitCache.set(rateLimitKey, newCount); try { // If the KV store exists, update the data in the KV store await env.RATE_LIMIT_KV.put(rateLimitKey, newCount, { expirationTtl: 60 }); } catch (error) { console.error("Error updating rate limit in KV:", error); // Even if the KV update fails, it does not affect the continued processing of requests }
• Problem Solving:By introducing KV storage and rate limiting mechanism, the same IP can only request 10 times within 60 seconds. If the number exceeds this, the 429 status code (too many requests) will be returned, effectively preventing malicious requests and abuse, and the statistics will be cleared after 60 seconds to respond normally.
• Enhancements:The rate limit is calculated for each IP separately, which limits the access frequency and prevents the same IP from making too many requests. It can protect against some malicious traffic-spamming behaviors to a certain extent.
3. Cache Control
v1.0 version codeNo ETag or Cache-Control headers are set when returning responses. The caching strategy completely relies on the default behavior of Cloudflare or the browser.v2.0 version codeOptimized cache:
// **Set cache policy** const headers = new Headers(); headers.set("Cache-Control", "public, max-age=600"); // Browser cache for 10 minutes headers.set("CDN-Cache-Control", "public, max-age=604800"); // Cloudflare CDN cache for 7 days headers.set("ETag", randomItem.etag);
• Problem Solving: The Cache-Control header is set to specify that the response can be cached by the browser for 10 minutes. This helps reduce the server burden and achieves more precise cache control in conjunction with ETag. At the same time, by setting CDN-Cache-Control, the image can exist in Cloudflare's edge cache for 7 days.
• Enhancements: Although Cache-Control can reduce the frequency of access, it also allows the background image to be updated within a certain period of time. Combined with ETag and edge caching, user browsers and Cloudflare will handle caching more intelligently.
However, compared to configuring cache in the worker, I sincerely recommend using Cloudflare's cache rules (or page rules, but page rules are a bit wasteful and unnecessary) to implement it, which is simple and clear, as shown below:

If Cloudflare's image optimization features, such as Polish and Mirage, are enabled at the same time, the 10-minute cache policy of the local browser may become invalid:
• Polish and Mirage Any of these may change the content or format of the image, which may cause browser and CDN (Cloudflare) cache misses, resulting in new images being loaded each time.
• Polish This can result in images being in different formats (like WebP and original), and they may be different each time they are requested, which can cause cache misses.
• Mirage Different versions of the image may be returned due to different device and network conditions, resulting in cache inconsistency.
4. Remove the judgment based on the query parameter "?type=" and change it to static path judgment
From a security perspective, using a static path (such as/fallback_pc.jpg
and /fallback_mobile.jpg
) instead of query parameters to make Worker routing more concise (wallback.tangwudi.com/*
Too rough): Only these three specific routes are needed to clearly distinguish the device type, avoiding the reliance on query parameters:

At the same time, the methods of accessing the random image API, forcing the return of PC wallpapers, and forcing the return of mobile wallpapers have changed as follows:
• Visit https://wallpaper.tangwudi.com/fallback.jpg
, according to the access terminal type, returns a "corresponding random wallpaper URL" (the reason for this change is shown below).
• Visit https://wallpaper.tangwudi.com/fallback_mobile.jpg
, force return to "mobile wallpaper URL".
• Visit https://wallpaper.tangwudi.comfallback_pc.jpg
, force return to "PC wallpaper URL".
This not only reduces the risk of malicious users bypassing device identification by forging query parameters, but also effectively avoids the processing of unreasonable or malicious random paths, thereby improving overall security and system stability.
Although adding the rate limit function can alleviate the problem of worker usage exceeding the limit caused by malicious traffic flow to a certain extent, it is inevitable that the worker will exceed the limit and the background image cannot be opened after the limit is exceeded. The problem still needs to be faced.
In fact, there is a simplest way, which can be achieved without changing the v2.0 code: put a picture in the "root path" of the wallpaper bucket (the quotation marks are because for COS, the concepts of "root path" and "directory" in traditional file systems are just human illusions), assuming it is named fallback.jpg (corresponding to the previous access), as shown below:

Then, just set the link of the background wallpaper image to "https://wallpaper.tangwudi.com/fallback.jpg
" (just corresponds to the worker's route), for example, the page background setting of my argon theme:

Why can this setting solve the problem? Because the entry route for the worker is setwallpaper.tangwudi.com/fallback.jpg
, so this address can be processed by the worker. Once the worker exceeds the usage limit, it will go on strike. The failure mode of R2's wallpaper bucket is set to "Automatically open (continue) on failure":

Then the access to the background image changes from the image URL returned by the worker to directly accessing the following link:
https://wallpaper.tangwudi.com/fallback.jpg
", and this image address is real, so the background image redundancy is achieved after the worker usage exceeds the limit.
Note: It should be noted that although disaster recovery of background images after worker usage exceeds the limit is achieved, if the worker usage exceeds the limit due to a DDoS attack, then the subsequent attack traffic will directly hit R2, so the key is to make good settings related to DDoS protection on Cloudflare. Friends who are not familiar with it can refer to the article:Home Data Center Series CloudFlare Tutorial (V) DDoS Attack Introduction and CF DDoS Protection Configuration Tutorial.
5. Better error handling and enhanced log output
v1.0 version codeWhen handling errors, only simple logging is done and a 500 error is returned.Ultimate CodeThe error handling is more comprehensive and detailed:
console.error(' Error in Worker:', error); // Log detailed error log return new Response('Internal Server Error', { status: 500 });
• Problem Solving: In version 2.0, not only errors are captured, but also detailed errors are recorded in the log, making it easier to debug and troubleshoot. Although a 500 error is still returned, there is a more detailed error output.
v1.0 version codeThere is no log output in the debugger, so it may not be easy to get detailed information about the access.v2.0 version codeAdded logging output:
console.log("User-Agent: ", userAgent); // Record logs for easy debugging console.log("Selected Folder: ", folder); // Record logs for easy debugging
• Problem Solving: Added logging of User-Agent and selected folders to help developers track and debug requests and file selection logic.
3.2.5 Implementation of v3.0 worker code (effect to be verified, not recommended yet)
Version v2.0 is enough for normal personal bloggers, but there is a problem with "worker free request limit exceeded, accesshttps://wallpaper.tangwudi.com/fallback.jpg
I am still concerned about this issue: if the worker usage exceeds the limit due to a DDoS attack, the attack will directly hit R2, which always feels inappropriate. Therefore, it is best to further optimize the code to avoid this situation. Based on this consideration, the v3.0 version code was born:
export default { async fetch(request, env, ctx) { const bucket = env.WALLPAPER_BUCKET; // Bind Cloudflare R2 bucket const url = new URL(request.url); const pathname = url.pathname.toLowerCase(); // Get request path (ignore case) // **Block common crawler tools** const userAgent = (request.headers.get("User-Agent") || "").toLowerCase(); const cfRay = request.headers.get("cf-ray") || "unknown"; const botPatterns = [ "curl", "wget", "httpie", "python-requests", "scrapy", "postmanruntime", "go-http-client", "java/", "libwww-perl", "okhttp", "python-urllib", "apache-httpclient", "httpclient", "lwp::simple", "mechanize", "aiohttp", "axios", "reqwest", "puppeteer", "headlesschrome", "phantomjs" ]; if (!userAgent.trim()) { console.log(`[BLOCKED] Empty User-Agent - cf-ray: {cfRay}`); return new Response("Forbidden: Empty User-Agent", { status: 403 }); } if (botPatterns.some(bot => userAgent.includes(bot))) { console.log(`[BLOCKED] Bot Detected:{userAgent} - cf-ray: {cfRay}`); return new Response("Forbidden: Bot Detected", { status: 403 }); } // **Determine the folder (PC / Mobile)** let folder = "pc_img/"; // Default PC if (pathname.endsWith("fallback_mobile.jpg")) { folder = "mobile_img/"; // Force mobile wallpaper } else if (pathname.endsWith("fallback_pc.jpg")) { folder = "pc_img/"; // Force PC wallpaper } else { // **`fallback.jpg` needs to be identified based on User-Agent** const isMobile = /iphone|ipod|android|blackberry|iemobile|opera mini/.test(userAgent); folder = isMobile ? "mobile_img/" : "pc_img/"; } console.log("Requested Path:", pathname); console.log("Selected Folder:", folder); // **Initialize memory cache (Map)** const rateLimitCache = new Map(); // Used to cache rate limit data // **Rate limit (up to 10 requests within 60 seconds)** const clientIP = request.headers.get("CF-Connecting-IP") || "unknown"; const rateLimitKey = `rate-limit-{clientIP}`; // Try to get rate limit data from memory cache let currentCount = rateLimitCache.get(rateLimitKey); if (!currentCount) { // If not in cache, try to get from KV storage try { currentCount = await env.RATE_LIMIT_KV.get(rateLimitKey); if (currentCount) { currentCount = parseInt(currentCount); } else { currentCount = 0; } } catch (error) { console.error("Error fetching rate limit from KV:", error); // If KV read fails, default to 0 currentCount = 0; } } const newCount = currentCount + 1; // If rate limit exceeded, return 429 error if (newCount > 10) { return new Response("Too Many Requests", { status: 429 }); } // Update memory cache and KV storage rateLimitCache.set(rateLimitKey, newCount); try { // If KV RATE_LIMIT_KV.get(rateLimitUsageKey); if (rateLimitUsage && rateLimitUsage > 90) { console.log("Worker usage exceeds 90%, caching fallback.jpg at edge"); // **Calculate time until midnight** const now = new Date(); const midnight = new Date(now); midnight.setHours(24, 0, 0, 0); const ttl = 24.setHours(24, 0, 0, 0); // **Check Worker usage** const rateLimitUsageKey = "rate-limit-usage"; let rateLimitUsage = await env.RATE_LIMIT_KV.get(rateLimitUsageKey); if (rateLimitUsage && rateLimitUsage > 90) { console.log("Worker usage exceeds 90%, caching fallback.jpg at edge"); // **Calculate time until midnight** const now = new Date(); const midnight = new Date(now); midnight.setHours(24, 0, 0, 0); } // Cache fallback.jpg to the edge until midnight ctx.waitUntil( caches.default.put(fallbackImageUrl, fallbackResponse.clone()) ); return fallbackResponse; } try { // **Get a list of files in the R2 bucket** const objects = await bucket.list({ prefix: folder }); const items = objects.objects; if (items.length === 0) { return new Response("No images found", { status: 404 }); } // **Randomly select an image** const randomItem = items[Math.floor(Math.random() * items.length)]; if (!randomItem) { return new Response("No valid images", { status: 404 }); } const imageUrl = `https://wallpaper.tangwudi.com/${randomItem.key}`; // **Set cache policy** const headers = new Headers(); headers.set("Cache-Control", "public, max-age=600"); // Browser cache for 10 minutes headers.set("CDN-Cache-Control", "public, max-age=604800"); // Cloudflare CDN cache for 7 days headers.set("ETag", randomItem.etag); // **302 redirect to image URL** return Response.redirect(imageUrl, 302); } catch (error) { console.error("Error in Worker:", error); return new Response("Internal Server Error", { status: 500 }); } } };
In this code, the query of the worker's free quota is mainly added: if it exceeds 90%, fallback.jpg will be cached in Cloudflare's CDN (the cache time is determined by the time difference between the current time and 0:00 in the morning, because the worker's free quota will be reset at 0:00, so it is enough to just hold on until that time point~):
// **Check Worker usage** const rateLimitUsageKey = "rate-limit-usage"; let rateLimitUsage = await env.RATE_LIMIT_KV.get(rateLimitUsageKey); if (rateLimitUsage && rateLimitUsage > 90) { console.log("Worker usage exceeds 90%, caching fallback.jpg at edge"); // **Calculate time until midnight** const now = new Date(); const midnight = new Date(now); midnight.setHours(24, 0, 0, 0); const ttl = (midnight.getTime() - now.getTime()) / 1000; // TTL until midnight in seconds // **Write fallback.jpg to edge cache** const fallbackImageUrl = "https://wallpaper.tangwudi.com/fallback.jpg"; const fallbackResponse = await fetch(fallbackImageUrl); if (!fallbackResponse.ok) { console.error("Failed to fetch fallback image for caching"); return new Response("Error caching fallback image", { status: 500 }); } // Cache fallback.jpg on the edge until midnight ctx.waitUntil( caches.default.put(fallbackImageUrl, fallbackResponse.clone()) ); return fallbackResponse; }
In this way, since the cache rule has a higher priority than the worker in Cloudflare's traffic sequence, subsequent access to fallback.jpg will no longer hit the worker, but will hit the cache rule. Although this temporarily loses the random effect of the image, it protects R2, and after 0:00 am, the worker quota is reset and everything is fine again!
Note 1: We have not yet had the opportunity to test whether this code is feasible or not. It is unknown whether it will be effective, and it may cause additional consumption of KV's daily free quota. Therefore, generally speaking, you can just use the v2.0 version of the code.
Note 2: In the latest v3.0 version of the code, normal use still only requires pointing the background image tohttps://wallpaper.tangwudi.com/fallback.jpg
If you want to force the use of PC wallpaper, just point to/fallback_pc.jpg
, and if you want to force only use mobile wallpaper, point tofallback_mobile.jpg
That's it.
Note 3: If you don't want the worker's free quota to be easily consumed, it is recommended to set up custom rules for Cloudflare WAF to prevent hotlinking and block direct access to the random image API, so that only access requests containing specific referrers can reach and be processed by the worker.
Note 4: This optimization can be continued, such as rate limiting plus penalty measures: how long will the blacklist be for more than 10 visits within 60 seconds; for example, using workers to convert images to webp/avif and return the best format based on browser support; for example, customizing image rules (such as returning sunshine images in the morning and night views in the evening); for example, adding image hotlink protection, returning default placeholder images, etc. However, these functions either have ready-made options that can be implemented directly (Cloudflare's various rules, wordpress theme options, etc.), or are not attractive to ordinary friends, so there is really no need to make the worker code too complicated. After all, complex function implementations are achieved by consuming the worker's free request quota, and the number of free requests is "unusable in normal times, and not enough in abnormal times" ~, so it is enough to update the code in the article to version v3.0.
4 Conclusion
Build your own random image API. Depending on each person's actual environment and preferences, the specific operation details may change more or less. For example, if you think some wallpapers are more suitable for day mode and not suitable for late night mode, you can further divide them into 4 directories according to day, night, PC, and mobile:/pc_img_day
,/pc_img_night
,/mobile_img_day
,mobile_img_night
, put in the correct picture, and then optimize the code to determine whether it is daytime mode or night mode, whether it is a PC or mobile device according to the specific time, and then return the correct picture URL; for example, in order to use different pictures every month, you can directly create 12 directories according to the month, and create PC and mobile directories in each directory, etc. Regardless of whether it is local storage or cloud storage, whether it is using index.php or cloudflare worker, the idea is the same. After the demand is determined, just find AI for the code part, which is very simple.
Currently, the background image of the blog has been set to a random image API based on the worker (v3.0 version code). The specific worker data can also be seen on Cloudflare:

In addition, there are additional discoveries after changing the background image from a fixed address to a random image API. After several tests, the PageSpeed Insights test score reached an all-time high of 80 points, which was a pleasant surprise:

考虑的很周到,学习了。我因为基本是自用(一些网页自用样式替换背景图),所以用的是微博图床储存图片,用CF woker实现随机图并反代修改Referer解决防盗链。也没做限流和图片缓存进CDN什么的。
其实限流、图片缓存CDN这些功能也不是必须的,在cloudflare上直接就可以实现。worker代码还是越简单其实越好,我后面那个v3.0版本的worker代码上线后都收到了kv使用超过50%的邮件通知了~~。
公开API还是考虑各种限制为好,之前我的图床(后端对接的是OneDrive)没做好速率限制,因为API速度率反复超限,被微软直接把整个组织扬了,损失了4位数的子账户
公共的那肯定了,我说的仅仅是自用的情况,如果要放开,那考虑的就不一样了。