All notes

Astro + Workers: Deploying a Content Website

From Markdown to static HTML to the edge: keeping content, builds, and delivery separate.

English edition, edited for length. Read the complete Chinese original.

There are at least three conversions involved in the process of a Markdown article being delivered to a reader’s browser: Astro reads the content and generates the page, Wrangler uploads the specified build directory, and Cloudflare delivers the resources based on the requested path. A successful deployment only proves that one of these steps has been completed, but it does not guarantee that the new article exists, that private drafts have not been leaked, or that the custom domain is correctly pointing to the delivered content.

This document explains the static blog structure using Tony’s Notes, illustrating how this architecture works. The goal is to clarify where each file is generated, who publishes it, and which layer to check for errors. The code implements a static content solution, without including login, database queries, or Astro server-side rendering. When adding dynamic capabilities later, it will be necessary to re-evaluate the boundaries.

1. Determine the architecture based on the frequency of request changes.

Assume the article is modified once a day, but readers access it many times throughout the day. If the HTML is finalized upon publication, there’s no need to repeatedly read the Markdown, query the content, and execute the template with each request. By generating the result once during the build phase and delivering the file during the runtime phase, we can proactively identify content errors and reduce the website’s reliance on the database and application processes.

The term “static” here refers to the point in time when the content is generated, rather than the level of interactivity. Features like search filtering, theme switching, or code copying can be performed in the browser. Modules requiring real-time data can call independent APIs. However, the fact that a component requires interaction does not mean that the entire page must be implemented using SSR. It’s more effective to first determine when the data is finalized, and then determine which JavaScript is needed in the browser, rather than first choosing a specific runtime architecture.

An example is a private account page: even if the page’s appearance remains unchanged over time, the content depends on the current user’s identity. Pre-generating it as a public file would not provide access control. Similarly, form writing, payments, and database transactions require a trusted server-side boundary. The experience of caching public articles cannot be directly applied to these requests, and using hidden links within the browser as a form of authorization check is also not possible.

2. Using Directories to Express the Boundaries Between Public and Private

The project’s task allocation can be easily verified:

src/content/writing/  Articles included in Astro content collections
src/pages/            Pages and routes
public/               Files copied unchanged into public output
.drafts/              Local private manuscripts excluded from public builds
dist/                 Current build output uploaded by Wrangler

The most easily misused feature is public/: it’s not a “directory for easily accessing documents,” but rather an open resource entry point. Placing drafts, internal attachments, or diagnostic results there means that even if the homepage doesn’t have a link, someone might still be able to find them by guessing the path. Similarly, Git ignoring a file only controls version tracking and won’t prevent the build script from copying it.

Therefore, private drafts are placed in the project root directory under .drafts/ and are not loaded by the content collection. They are also not located within public/. The publishing process only copies the selected articles to the public content directory. This is the content workflow for this project, not a private backend automatically provided by Astro. Directly packaging and deploying the entire draft directory would bypass all status tags.

The audit should follow the generated artifacts, rather than just the location of the source files. After the build, verify that dist/ does not contain private files, unpublished titles, or draft routes. Code generators, full-text search indexes, and RSS feeds can all represent additional output points. When adding new functionality, these should be re-checked to ensure they are actually reading the correct sets of data.

3. Separate Build Configuration from Delivery Configuration

The site string represents the official website address, and it’s used to generate links for site maps and other integrated content. Since the current project uses static output and doesn’t require an SSR adapter to use Workers, the simplest configuration is maintained to minimize the mixing of runtime and static content concepts.

import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';

export default defineConfig({
  site: 'https://taotao.dev',
  integrations: [sitemap()],
});

site does not automatically inject a complete canonical URL for each page, nor does it automatically set up DNS or redirect www to the root domain. Page layout, RSS implementation, sitemap, and domain configuration all have their own consumers, and each needs to be checked individually. You cannot assume that all generated links are correct simply because a formal domain name is listed in the configuration.

Wrangler is responsible for resource uploading and deploying Workers.

{
  "name": "taotao-dev",
  "compatibility_date": "2026-10-11",
  "assets": {
    "directory": "./dist",
    "not_found_handling": "404-page"
  }
}

This is an excerpt; the actual project also includes custom domain configuration. directory specifies the content to be uploaded, but not the Astro output format; both sides must be consistent. A purely static deployment without a Worker script entry is delivered by the platform and does not require writing a meaningless forwarding function. The compatibility date is not the software version or release date; when changing it, check the documentation for the corresponding behavior.

4. Each build should result in an understandable output.

First, install the necessary dependencies in a Node environment compatible with the project. When a lock file already exists in the repository, automated environments typically use npm ci to ensure that the parsed results follow the lock file. If the dependency manifest and the lock file are inconsistent, first resolve the inconsistency; do not silently obtain a different set of versions by re-parsing.

npm ci
npm run check
npm test
npm run build

Here are four steps to answer different questions:

  1. Install dependencies according to the lockfile.
  2. Perform type checking to identify code issues.
  3. Run tests to verify adherence to conventions.
  4. Build and generate resources that are actually delivered.

Passing tests does not guarantee that the current build produces the correct output. A successful build does not guarantee that the code is factually correct. The content schema can detect missing fields, but it cannot verify the logic or references within the code examples to confirm whether they support the conclusions.

After building, check the HTML, RSS, and sitemap of the target article, and then open the local preview. Instead of simply counting the number of pages, verify that the title, date, links, and code blocks match your expectations. When adding a new article, if the number of output pages remains the same, you need to determine whether it’s due to draft filtering, slug conflicts, files not being matched by the loader, or whether you’re currently viewing the previous build.

It’s best to keep the release workspace separate from the workspace where you’re actively writing. If changes are made to files during the build process, the upload might result in a mixed state, neither the old commit nor the new commit. Manual publishing should involve checking the working area before and after the build, while automation should clearly record the relationship between commits and outputs to avoid relying solely on version numbers for traceability.

Tracking Build Inputs from the Same Article

Assume that after modifying the main content, a new summary appears on the local homepage, but the generated article’s HTML still contains the old content. Do not adjust the Cloudflare cache first: at this point, the old content already exists in the unuploaded output, and the network layer is not the first point of failure. First, check the project directory where the command is running, the content collection matching path, whether the article is still being filtered, and whether it’s reading the current output directory. Pinpointing the earliest unexpected conversion will reduce unnecessary configuration changes.

Conversely, if the generated HTML is correct and the default Worker address serves it correctly, but the official domain returns the wrong content, the content parsing stage already has a positive example. There is no need to redesign the Markdown loader. Instead, verify the actual bindings, routes, and the content returned by the official domain name. Each layer should use the same, distinctive article. Proving the build with the homepage and deployment with a different article would leave no complete end-to-end chain verified.

The input used for testing should also be reproducible. When an article references an external API to obtain build data, the same source code may generate different results at different times. Changes during installation, such as dependencies or upgrades to image converters, can also alter the delivered resources. Static output does not guarantee build determinism. For inputs requiring strict traceability, record their versions or retain snapshots approved for public use. Clearly define whether the build should be aborted or use older data in case of external failures. It is not acceptable to silently treat empty results as the latest content.

5. The 404 strategy is part of the blog routing contract.

When a blog post does not exist, the reader should see a “page not found” message, not the homepage. SPA (Single Page Application) fallback is suitable for applications where the client controls the routing, but it can cause a misconfigured article URL to return to the homepage and a success status. Search engines, citation tools, and automated checkers cannot determine whether the article actually exists.

This project utilizes 404-page, and provides the Astro 404 page. However, all requests still require online validation: the paths for local development servers, Astro preview, and Workers static assets are not the same. In particular, pay close attention to article links with and without trailing slashes, as well as paths with non-existent file extensions.

Verification requests should also distinguish between HTML pages and independent resources. /writing/astro-workers/ should return the article, and a script path in the resource directory should return the corresponding file. Missing scripts must not return HTML disguised as a successful response. The browser may only report syntax or MIME issues when parsing the script, but the real problem is that the resource path is using an incorrect fallback. Recording the request URL, status, content type, and body identity often provides a more accurate understanding of the root cause than simply looking at the last error message in the console.

Query parameters are typically not used to create a separate Markdown file, but they may be involved in how the browser or additional caching rules handle the content. When checking links with parameters, it’s important to preserve the original article identity and verify that redirects are not inadvertently dropping the parameters. Also, for fragment identifiers, you should know that they are generally handled by the browser and are not sent to the server with HTTP requests. Modifying the server routing cannot directly fix internal errors within a page, and checking the chapter ID and link within the final HTML is necessary.

curl -I https://taotao.dev/writing/astro-workers/
curl -I https://taotao.dev/writing/this-article-does-not-exist/
curl -I https://taotao.dev/.drafts/index.html

The first request should reach the specified article. The subsequent two requests should refuse to provide an article or private draft. While HEAD can quickly check the status and headers, it cannot verify the body’s identity. If necessary, use GET to check the title and content. Returning a visually appealing error page with a success status does not satisfy the missing-resource contract.

6. Custom Domain Requires Independent Verification

In the Workers configuration, custom_domain expresses the intent to bind, but the actual operation also depends on Cloudflare account permissions, the zone where the domain is located, and existing configurations. Before making changes, verify that the target domain is not hosting other services and confirm that the new binding will not replace any existing entries that need to be retained. It’s not necessary to attribute every domain issue to Astro.

The troubleshooting process can be divided into three layers: address resolution, connection and certificate, and HTTP response. The first layer involves determining the destination address and explaining where the initial request was sent. The second layer involves verifying whether the TLS connection was successful and explaining whether the connection was established. The third layer involves examining the content of the returned HTTP response to determine the final application artifact. Combining this information into a single browser error screenshot can lead to repeated adjustments at the error level.

If both the root domain and www are available, define the canonical address. Having both URLs does not necessarily mean that one will automatically redirect, nor does it imply that the canonical URL has been unified. Choose one public address, and then implement the redirection within the relevant platform rules or application routing. Also, verify the handling of status, target, and query parameters. Avoid a loop in which the root domain redirects to www and www redirects back to the root domain.

Changes to DNS and certificates may not be immediately observed by every network. Record the check time and entry point, and obtain evidence from at least one actual user access path. The “bound” status displayed in the console alone cannot establish that the website is available. Similarly, do not consider a result obtained by ignoring certificate errors a successful public availability test.

7. Cache strategies should align with the file lifecycle.

Resources with content fingerprints can be long-term cached because changes to the content will result in new URLs. HTML URLs typically remain unchanged, so it’s important to consider how readers will obtain the new version when updating. We cannot simply set long caching durations for all paths to reduce requests, and then expect that articles will be immediately updated after each deployment.

The platform’s default caching, browser caching, and any additional rules should be checked separately. When troubleshooting outdated content, first verify the deployed version and request path, then compare the response headers and body. A single forced refresh only indicates that this specific access path is affected by caching, and does not prove that other readers have switched to a different version. Also, do not clear the cache of all irrelevant domains in order to temporarily fix a specific article.

For this website, the main article content, lists, RSS feeds, and sitemap are consumers of the same release. If the main article content has been updated but the feed remains on an older version, readers may still miss new content. Checking the update relationships between these entry points is more accurate than simply checking the homepage, as it provides a closer reflection of the actual publishing results. Furthermore, third-party RSS readers cache content, which is not fully controlled by the website. Therefore, when drawing conclusions, it’s important to acknowledge this boundary.

Avoid Losing Object References When Updating HTML

If HTML references scripts with fingerprints, the two files represent a single logical result. When older pages are still active in the browser, they may continue to request the old fingerprint addresses. New pages should reference the new resources. To ensure a smooth transition, it’s crucial to verify how new deployments provide access to historical resources, whether a caching layer still delivers the old HTML, and so on. It’s not sufficient to assume that simply changing filenames with fingerprints will automatically resolve the entire version switching protocol.

Static articles typically require less client-side scripting, thereby reducing the dependency on this type of integration. However, styles, images, and fonts still have similar dependencies: the page must return the correct title, the image paths must be valid, and the reading experience must remain complete. To obtain the actual reference list for the representative page after publication, it is necessary to check the status and content type of the necessary resources, and not to assume that a successful GET request for the article means that all resources are accessible. External image services represent another availability path, and this site cannot guarantee continuous response for them.

To diagnose caching issues, it’s crucial to clearly define the observation conditions: which URL, whether the user is logged in, whether the browser has cached the content, whether an additional proxy is being used, and which content identifier is observed. Introducing random parameters or clearing all caches can temporarily reveal new content, but it also obscures the actual user’s path. It’s important to first save the original response data and then modify a single condition in a controlled request to determine whether the difference is due to the browser, the platform, or the application version. Instead of assuming that a random change is the root cause, it’s essential to identify the true cause.

8. Introduce a Runtime Only When Dynamic Capabilities Are Needed

When adding contact forms or interfaces requiring server-side permissions in the future, consider evaluating the independent Worker API or introducing Astro’s server-side capabilities for appropriate routing. The choice depends on the boundaries of data, authentication, deployment, and maintenance. Avoid re-querying the database for all existing articles that are already suitable for static delivery, simply to achieve a uniform appearance.

The runtime secret must be managed by the server-side environment, and should not be written into public/ or any build constants that are read by the browser. If a specific environment variable only exists on the local .env, it does not mean that its value will not be included in the client package. Checking for the presence of secrets in the final output is a more direct approach than simply checking if a file has been ignored by Git.

Dynamic interfaces also provide error propagation, logging, rate limiting, and access control. They cannot be replaced by the static 404 strategy. When adding a new interface, it is important to maintain the existing acceptance criteria for static articles, while also adding the interface’s own authentication, input, and failure testing. This ensures that architectural extensions have clear evidence, rather than simply extrapolating existing success conclusions indefinitely.

Even with small-scale dynamic updates, it’s crucial to clearly define the data flow.

For example, adding a “favorite” button to static articles, the browser sends user actions, and the API stores the current user’s favorite relationships. The main content of the page is still delivered from static resources. To decide how to send login credentials, whether cross-origin access is allowed, and how to handle errors and duplicate clicks, a single boolean value cannot be stored solely within the button component. The browser displaying “favorited” and the server confirming the write are different facts. Also, whether the status is restored after refreshing is part of the API protocol.

Additionally, check whether public and personalized responses have accidentally been assigned the same caching strategy. Articles can be publicly reusable, but a user’s saved list must not be shared with others simply because the URL appears fixed. Furthermore, private results should not be written into HTML during the build, as runtime authentication cannot undo that disclosure. It is preferable to treat public content, identity queries, and write operations as separate requests, with distinct caching and permission mechanisms, rather than having all data pass through a single, ambiguous “page interface.”

Dynamic interface changes also require a separate failure budget. When the API is temporarily unavailable, the following factors are important for the product: whether the article can still be read, whether the button clearly indicates failure, and whether the intention to safely retry later is preserved. The advantages of a static site can be retained: non-critical interactions can fail without causing the article to disappear. For critical writes, optimistic writing cannot hide unknown results. The value of architectural splitting is clear fault boundaries, not just splitting files into two directories.

Do not share vague release eligibility between preview and final products.

Initially, the same dist directory is used for public preview, followed by uploading the official Worker. This means that the preview also involves delivering real content. The noindex directive can express the intention not to be indexed, but it does not make the article private. If the preview contains unapproved content, it requires a properly controlled access point; uploading the official version only accepts content that has been approved and made public. The environment name “preview” cannot replace this boundary.

For acceptance, use a synthetic document approved for publication and a private sentinel to verify each environment’s permitted content scope. When reusing artifacts across environments, first prove that their publication eligibility is consistent, then check the domain and response policies. Do not assume that a test version is the same as the official version simply because the directory names are the same.

9. Completing End-to-End Testing with a Single Article

First, fill in the source path, output path, and official address for the target article. Then, provide the results of each check item. The source file belongs to a public collection, the output includes a specified title, and the official GET returns the same content. RSS and sitemap use the correct address. If a step fails, stop at that point to troubleshoot and prevent subsequent successful operations from covering previous gaps.

The negative example uses a draft that was not selected. This site’s isolation script recursively checks the output, rejecting draft directories and searching for titles, filenames, and detail routes within the inspected text files. It does not scan arbitrary body fragments, compressed files, or every secret, and therefore cannot establish the absence of all leaks. When copy scripts, search data, or attachment handling change, add checks for the new output paths.

Finally, record the commit, Worker, and deployment version along with the observation time in the same entry. Without online results, the conclusion can only state that the artifact has been generated or the platform has received it. Without an isolation negative example, a reachable public article does not establish the safety of private content. The value of the record is to inform the next maintainer which pieces of evidence already exist and which need further investigation.

References