About This Site
Ink · Code documents my learning, practice, and thoughts on Web development, along with explorations and experiments with related technologies.

Ink · Code is my personal technology blog, mainly documenting my learning, practice, and thoughts on Web development. It includes learning notes ranging from the basics to hands-on practice, as well as problems I encounter during development and the solutions I find.
Writing is also part of how I learn. Reorganizing what I have learned and expressing it again in my own words often reveals gaps or fuzzy areas in my understanding, while gradually connecting scattered pieces of knowledge. At the same time, building and maintaining the blog myself gives me more opportunities to put Web technologies into practice.
The site was originally inspired by the design ideas of QuietPages. As my needs have evolved, I have continued to adjust the site’s design and features. This blog is not only a place to record what I learn; it has also become part of how I learn and practice Web development.
Page Overview
The site is centered around reading and presenting content, with pages for browsing articles, content archives, statistics, contact, copyright information, and more.
Browsing Content
The home page mainly presents the latest content and featured articles, making it easy to see recent updates at a glance. The Hero section supports background switching, with Unsplash or Lorem Picsum available as image sources.
The article list is used to browse and search articles. Articles can be filtered by category, tag, and collection, with sorting and pagination also available. On an article page, you can continue reading through previous/next navigation and related articles, or join discussions in the Giscus comment section.
The archive organizes all articles by time and provides three views: year, month, and list. The year view shows article distribution and publishing frequency by year; the month view uses a calendar to show which dates articles were published; and the list view groups articles by publication time, making it suitable for chronological browsing and review.
Site Information
The statistics page summarizes article counts and word counts, publishing activity, category composition, access trends, visitor locations, and other data. Charts on the page are built with ECharts, while geographic distribution is displayed on a map. When an AMap security key is configured, the site uses AMap JS API 2.0; otherwise, it falls back to Leaflet with map tiles provided by CARTO.
The contact page provides an in-site email feature. You can preview the message before sending it, and email delivery is handled through Resend.
The copyright notice records the sources and license information for fonts, images, textures, and third-party resources used on the site.
External Resources
Bookmarks is used to organize frequently used websites by category, with search and filtering support.
Friends showcases personal sites that I follow over the long term and also provides an entry point for submitting link-exchange requests.
Visuals and Browsing Experience
The site’s appearance is controlled by several groups of settings: skins switch the theme color scheme, light/dark mode adjusts the overall brightness, and textures add detail and character to page backgrounds. Fonts for headings and body text can also be selected independently. Font resources are provided by ZeoSeven Fonts, and the body font size can be adjusted to suit different reading preferences.
Rough Notation adds hand-drawn annotations to headings, buttons, and highlighted content. When the page layout changes, the annotations recalculate their positions.
A small number of UI sound effects are also included to accompany page interactions. The sounds are kept as closely synchronized with interactions as possible, but factors such as audio loading, browser scheduling, and the system playback pipeline may cause noticeable delays on the first playback or when a sound is triggered again after a long period of inactivity.
In-site navigation uses Astro ClientRouter for client-side routing, with custom page transition animations based on View Transition to reduce the visual discontinuity between page navigations.
The site uses Lenis for smooth scrolling. Components with independent scroll areas—such as the table of contents, comment section, editors, and settings panels—retain native scrolling to prevent scroll behaviors from interfering with one another.
Code Experiments
Code in articles can appear in three forms: inline code, multi-line code, and runnable code.
Inline code is processed by rehype-pretty-code. The plugin reads the language marker in the code and uses Shiki for syntax parsing, ultimately generating HTML with token styles.
The language marker is written at the end of inline code. For example, `const count = 1{:javascript}` is rendered as const count = 1. When a language is specified, the code is highlighted using the corresponding syntax; inline code without a language marker keeps the basic style.
For multi-line code, use a Markdown code block with a language marker, for example ```javascript. It uses the same highlighting engine as inline code, but enters through a different processing path and does not go through rehype-pretty-code’s inline-node transformation.
const count = 1;
console.log(count);
Articles can also embed runnable code experiment components through MDX, allowing readers to modify the code and run it in real time. These components are injected globally by the article page, so they do not need to be imported again when used in MDX.
Front-end Playground
The front-end Playground is built with Sandpack and is mainly used to run and demonstrate browser-side code such as HTML, CSS, JavaScript, and front-end components. It supports multi-file editing and provides features such as live preview, console output, code formatting, and layout adjustment. Each example can also specify the file that opens by default and whether the preview or console is shown initially.
<Playground
codeId="about/frontend-playground"
title="Front-end Live Preview"
activeFile="/App.js"
initialPane="result"
/>
Below is an editable and runnable example. After modifying the files on the left, you can view the result in the preview or console on the right.
Remote Code Runner
The remote code runner uses CodeMirror to edit source code and forwards execution requests to the remote execution platform selected by the visitor, such as Judge0 or Wandbox. It is suitable for running languages such as Python, Java, and C++ that require an interpreter or compiler.
<RemoteCodeRunner
codeId="about/python-remote-execution"
title="Remote Python Execution"
language="python"
stdin="Ink · Code"
/>
Below is an editable and runnable example. Configure standard input and run the code to view the result in the output panel.
Remote Python Execution
选择平台并点击“运行”,结果会显示在这里。
To prevent abuse of remote execution services, securely store credentials such as each platform’s API Key and Token during deployment. Do not expose them in browser-side code or commit them to a public repository.
Different execution platforms have their own quotas and service limits. Once a limit is reached, code execution may stop working; in that case, switch to another execution platform and try again.
Browser-side SQL Playground
The browser-side SQL Playground runs SQLite directly in the browser using sql.js and WebAssembly, without connecting to a remote database. It supports continuous create, read, update, and delete operations, and can also run only the currently selected SQL fragment when needed.
To make common MySQL examples easier to demonstrate, I added a lightweight compatibility layer before SQLite execution that converts some MySQL syntax into forms that SQLite can execute. Statements that the compatibility layer cannot handle are passed directly to SQLite; if they contain syntax unsupported by SQLite, the corresponding error is returned.
When multiple components use the same databaseId, they can share the same browser-side database. Database snapshots are stored in the current browser’s IndexedDB, making it convenient to continue experiments across different articles or sections.
When databaseId is not set, each component uses an independent temporary database, which is suitable for standalone SQL examples that do not need to preserve state.
<SqlPlayground
codeId="about/sqlite-local-execution"
title="Local SQLite Execution"
/>
Below is an editable and runnable example. You can execute multiple statements at once or run only the selected SQL statements.
Local SQLite Execution
Project Structure
The following lists the project’s main directories and key files, with a brief description of their respective purposes.
Project Structure
srcThe main source directory for the site.
- assetsImages and other assets processed by Astro during the build.
- codesRunnable code used in articles, referenced by stable codeIds independent of article titles.
componentsFeature components used throughout the site.
- analyticsComponents related to visit analytics.
- archiveComponents related to the archive page.
- blogComponents for article lists and article detail pages.
- code-runnerComponents for the remote code runner.
- commonShared base components, scripts, and preload logic reused across pages.
- homeComponents related to the home page.
- layoutComponents related to page layouts.
- mdxInteractive components and wrappers used by MDX articles.
- playgroundSandpack-based components for front-end code editing and live previews.
- sql-playgroundBrowser-side SQL execution and experiment components based on SQLite.
- statisticsComponents for aggregating and visualizing site content and visit data.
- uiShared basic UI components such as buttons, icons, cards, and images.
- widgetsRelatively independent functional widgets.
- configsEditable configuration for the site, fonts, code themes, maps, hand-drawn annotations, and sounds.
contentArticles and structured data used by Astro Content Collections.
- authorsJSON files containing author profiles.
- blogsMarkdown/MDX technical articles organized by topic and series.
- bookmarksJSON files containing bookmark categories and site information.
- friendsJSON files containing friend-link information.
- file-treesFile-structure data used by the FileTree component.
- layoutsPage layouts, global Head, content containers, and shared client-side logic.
- libShared data access, utility functions, and browser lifecycle logic.
- pagesAstro page routes, dynamic routes, and server-side APIs.
- stylesDesign tokens, global styles, themes, textures, and page-specific styles.
- typesShared TypeScript type definitions for the project.
- content.config.tsCollection, schema, and data-loading configuration for Astro Content Collections.
- middleware.tsRequest-level security headers, the AMap proxy, and other runtime middleware logic.
publicStatic assets published directly without build processing.
- masksMask image assets used for page visual effects.
- soundsSound files used to enhance page interaction feedback.
- stylesGiscus comment-section styles published as static files.
- texturesOptional paper, fabric, and sandpaper background textures.
migrationsCloudflare D1 database migration files.
- 0001_init.sqlCreates the base tables for analytics and visitor data.
- 0002_analytics_indexes.sqlAdds indexes to fields used by analytics queries.
- 0003_code_run_rate_limits.sqlCreates the rate-limit table and indexes for remote code execution.
scriptsHelper scripts used when creating articles and during local development.
- create-doc.mjsInteractively creates a Markdown/MDX article with frontmatter.
- .dev.vars.exampleExample configuration for local Worker runtime variables and Secrets.
- .env.exampleExample configuration for Astro/Vite build-time and public browser variables.
- astro.config.tsThe main Astro project configuration for framework integrations, build options, Markdown processing, the deployment adapter, and related features.
- worker-configuration.d.tsTypeScript type declarations for the Cloudflare Workers runtime and resource bindings.
- wrangler.jsoncCloudflare Wrangler configuration defining Worker deployment parameters, runtime environment, resource bindings, and static assets.
Site Configuration
Site Information
The site’s basic configuration is centralized in src/configs/site.ts:
const siteConfig = {
name: "Ink · Code",
description: "A personal technology blog built with Astro for documenting technical explorations and daily development",
url: "https://example.com",
author: {
email: "you@example.com",
location: "Your city",
coordinates: [30.63, 104.0], // [latitude, longitude]
},
clientRouter: true,
codeTheme: {
light: "catppuccin-latte",
dark: "one-dark-pro",
},
giscus: {},
nav: [],
footerNav: [],
};
Internationalization
Internationalization is configured in src/configs/i18n.ts.
const i18nConfig = {
enabled: true,
defaultLocale: "zh-CN",
locales: ["zh-CN", "en", "ja"],
fallback: {
en: "zh-CN",
ja: "zh-CN",
},
routing: {
prefixDefaultLocale: false,
},
};
enabled: Controls whether internationalization is enabled. When disabled, the entire site uses onlydefaultLocale;defaultLocale: Determines the site’s default language and which language is used for URLs without a locale prefix;locales: Determines which languages the site supports. Locale codes are used as URL prefixes. When adding a new language, also extendSUPPORTED_LOCALESand the copy dictionary insrc/types/i18n.ts;fallback: Determines which locale URL to redirect to when a translation for a language is missing. For example,{ en: "zh-CN" }means that if the English translation does not exist, the corresponding Chinese page is used instead. If no fallback mapping is configured, a 404 is returned directly;prefixDefaultLocale: Controls whether the default language uses a/<locale>/prefix;
Map Services
Map services are configured centrally in src/configs/map.ts. During local development, public configuration required by the browser is placed in the project-root .env, while security keys used at Worker request runtime are placed in .dev.vars. The two files serve different purposes, so the same variable should not be duplicated in both. For Cloudflare production configuration, see Deploying to Cloudflare below:
.env: Public configuration needed at build time and in the browser.
PUBLIC_AMAP_KEY=Your AMap Web API key
PUBLIC_CARTO_KEY=Your CARTO Basemap key
.dev.vars: Secrets used by the local Worker at runtime.
AMAP_SECURITY_CODE=Your AMap security key
PUBLIC_AMAP_KEY is the Web key for AMap JS API 2.0 and must be sent with browser requests. AMAP_SECURITY_CODE is the AMap security key; both locally and in production, it is read server-side by the same-origin /_AMapService proxy and is not exposed to the browser.
If PUBLIC_AMAP_KEY is not configured, the component automatically falls back to Leaflet and CARTO. If a Key is configured but AMAP_SECURITY_CODE is missing, AMap proxy requests will fail, and the missing variable must be configured first.
PUBLIC_CARTO_KEY is used for the CARTO basemap service. Tile requests carry it in the browser, so it is likewise a public Key. It is recommended to apply for and configure one with CARTO to avoid watermarks on the map.
Deploying to Cloudflare
The project uses @astrojs/cloudflare to build for Cloudflare Workers, with Worker, static asset, and D1 database bindings declared in wrangler.jsonc.
This project uses the GitHub integration for Cloudflare Workers to deploy automatically. After connecting the repository, pushing code to the designated branch causes Cloudflare to build and deploy automatically. For detailed configuration, see the Workers Builds documentation.
Initial Setup
-
Create your own D1 database
pnpm exec wrangler login pnpm exec wrangler d1 create ink-code-analyticsWrite the returned
database_idand database name towrangler.jsonc, making sure to keepink_code_analyticsas the binding. -
Connect the GitHub repository in the Cloudflare dashboard
- New Worker:
Workers & Pages→Create application→Import a repository; - Existing Worker: open the Worker’s
Settings→Builds→Connect; - Select this repository and the production branch, and set the root directory to
/; - Set the build command to
pnpm run buildand the deploy command tonpx wrangler deploy; - Confirm that the Worker name in Cloudflare is
ink-code, matching thenameinwrangler.jsonc.
- New Worker:
-
Configure environment variables
Environment variables are divided into two categories: “build-time” and “Worker runtime”. They are read at different stages and must be configured separately:
- Build-time variables: read by
Astro/Vitefromimport.meta.envwhile runningpnpm run build. Locally, place them in.env; on Cloudflare, configure them underWorker → Settings → Builds → variables and secrets. Configure:SITE_URL: Production site URL;PUBLIC_AMAP_KEY,PUBLIC_CARTO_KEY,PUBLIC_AMAP_SERVICE_HOST,PUBLIC_HERO_IMAGE_PROVIDER: Public configuration that enters browser code or request URLs.PUBLIC_*must not contain Secrets;
- Worker runtime variables: read from
envincloudflare:workersafter the Worker receives a request. Locally, place them in.dev.vars; on Cloudflare, configure them underWorker → Settings → Runtime variables and Secrets. Configure:RESEND_API_KEY,RESEND_FROM_EMAIL: Read by the contact API at request runtime; store the API Key as a Secret;AMAP_SECURITY_CODE: AMap security key, read and attached to upstream requests by the/_AMapServiceproxy;UNSPLASH_ACCESS_KEY: Read by/api/hero-image/; the browser never receives this Key. The random-image endpoint does not requireUNSPLASH_SECRET_KEY;CODE_RUN_RATE_LIMIT_SALT: Rate-limit salt for the code runner; store it as a Secret;- Credentials and runtime endpoints for code execution platforms: see
.dev.vars.examplein the repository root for the complete variable names. Store all credentials as Secrets.
Build-time variables do not automatically become runtime
envvalues for the Worker, and runtime variables do not participate in the Astro/Vite build.PUBLIC_*values are compiled into browser code or request URLs and may contain only public Keys; never add thePUBLIC_prefix to any API Token, Client Secret, password, or salt.For local development, it is recommended to use both files:
.envonly for build-time and public configuration, and.dev.varsonly for Worker runtime configuration and Secrets. According to the Cloudflare local environment variable documentation, when.dev.varsexists, Wrangler does not merge values from.envinto the Workerenv; therefore, the same variable must not be written to both files. If you do not create.dev.vars, you can also use only.envas the single local source for build-time and runtime values, but all runtime variables must then be placed in.env, which is less suitable when Secrets need to be clearly isolated. - Build-time variables: read by
-
Before the first deployment, apply migrations to the remote D1 database. GitHub automatic deployment only builds and publishes the Worker; it does not run database migrations automatically:
pnpm exec wrangler d1 migrations apply ink-code-analytics --remoteDuring local development, you can first validate them with
--local. When new migrations are added later, run--remoteagain after confirming that the SQL is correct; do not modify or delete migrations that have already been applied—only add migrations with larger sequence numbers. See the D1 migrations documentation for details.
Routine Publishing
After modifying the code, run pnpm check, commit the changes, and push to the branch monitored by Cloudflare:
git add .
git commit -m "Update site"
git push origin main
Cloudflare will automatically build and deploy the site. If the build fails, first check the logs under the Worker’s Deployments → View build history.
Resources and Licenses
- Web fonts are provided by ZeoSeven Fonts;
- The Hero background source is selected by
PUBLIC_HERO_IMAGE_PROVIDER, with Unsplash and Lorem Picsum available; - Background textures come from Transparent Textures;
For resource licenses and usage notes, refer to the copyright page. If you find any issue with a resource source or usage, you can let me know through the contact page.
