@caltho/site-kit

中文文档
0.2.11发布于 2026/11/09MITTypeScriptES Module近 30 天下载 3691

What every Caltho site is built with: the contract with CalOS (content, and how a site declares what is editable), images, search and security, and the checks

原始使用文档
文档目录7 节
  1. The idea
  2. What's in it
  3. Sections
  4. The theme
  5. Share cards
  6. A build
  7. Working on it

site-kit

What every Caltho site is built with, published to npm as @caltho/site-kit. Each client's site is its own repository with its own design, started from site-starter. The kit is not a design system. It's the contract that lets CalOS manage any of those sites, and the plumbing every one of them needs.

The idea

A site's content (its pages and what's on them) lives in CalOS, and the owner edits it there. Publishing in CalOS rebuilds the site with that content: no one pushes code to change a page. The design, meaning how a block looks and what kinds of block there are, is the site's own code.

For CalOS to edit a site it hasn't seen, the site declares what's editable. That's all this takes:

// content/schema.ts
import { defineSite, image, link, text, z } from "@caltho/site-kit/content";

export default defineSite({
  hero: z.object({ heading: text, text: text.optional(), image: image.optional(), actions: z.array(link).default([]) }),
  faq: z.object({ heading: text, items: z.array(z.object({ q: text, a: text })).min(1) }),
  // …whatever this site's design has
});
  • A page is path, title, description, an optional menu label, and a list of blocks. A block is one of the kinds the site declared, with the fields it declared.
  • site-kit prepare checks the content against that, in every build, and says every problem with where it is (pages[0] (/) › blocks[1] (faq) › items[0] › q). A build never publishes a page with a hole in it.
  • It also writes .site/schema.json, the declaration as JSON Schema. CalOS draws its editing forms from that: text boxes, picture pickers (with required descriptions), lists to add to, reorder and remove. Add a kind of block to a site and it appears in CalOS with no work on CalOS's side.
  • A new page is a new entry in the list with a path; the site's catch-all route renders it.

Editing on the page. A site gives CalOS a page at /calos-preview that draws a draft with the site's own components, as CalOS sends it (SitePreview from @caltho/site-kit/next/preview):

// app/calos-preview/page.tsx
import { previewOrigins } from "@caltho/site-kit/next";
import { Preview } from "@/components/Preview"; // "use client": <SitePreview origins={origins} render={(page) => <SitePage page={page} />} />
export default function CalosPreview() { return <Preview origins={previewOrigins()} />; }

Components read content only through the kit (site(), image()), so the same components draw the built pages (from .site/prepared.json) and the draft (from CalOS). A click on the preview is matched back to the field it meant with nothing added to the components. Which block a click landed in is exact where the page puts <BlockMark at={i} /> (from @caltho/site-kit/next) before each block it draws: a block may then draw nothing (a list with nothing in it yet) or several elements. It draws nothing on the live site. Without marks, blocks are found among main's children, by their words when there isn't one for each. Only the business's own CalOS addresses, which the build is given with the content, may frame the page or send it anything.

Forms are separate and optional. A block can post to any address (a CalOS form, another service, or none); the kit only checks it's a URL.

What's in it

partwhat it does
Content (@caltho/site-kit/content)defineSite and the pieces to build blocks from (text, md, image, link, href, business, …); the checks, including that every link lands on a page and every #part exists; the JSON Schema.
prepareLoads the content from CalOS (CALOS_CONTENT_URL, CALOS_SITE_TOKEN; the site's own while nothing's published there) or content/site.json, sends CalOS the schema, checks it, writes the schema, and makes every picture the content names as WebP at the widths screens need plus a 1200×630 share crop, named by content so they cache for a year.
finishAfter the framework's build: a content security policy in every page (each inline script named by hash), _headers (security headers, long caching, noindex on preview addresses), and _redirects from CalOS (CALOS_REDIRECTS_URL; a build that can't reach CalOS fails rather than lose them). Where CalOS counts the site's visits (its content answer says where they go), the analytics script in every published page but the preview, with its addresses in the policy, and phone and map links marked so a tap is counted (phone_tap, directions_tap; a link marked data-track already is left as it is). No cookies. The standard privacy policy then leaves out its claim of no analytics, saying nothing in its place: what to say of the counting is the business's (extra).
checkEvery page in a real browser on a computer and a phone: fails on anything the policy blocks and anything that doesn't load. What axe finds (WCAG 2.0–2.2 A and AA, best practice) is said with where, for the site to weigh (a colour the business chose over the contrast rule is their choice); --strict fails on it too, --allow=rule,rule leaves named rules out.
SearchTitles that fit, canonical addresses, the business as schema.org data (hours, areas, address), FAQ and breadcrumb data, robots.txt, the sitemap.
PoliciesPrivacy, accessibility and terms, written from the business's facts and what the site does. Optional: a site calls policy() where it wants them.
Helpers for components (@caltho/site-kit/lib)The pieces that touch no files: md (Markdown that can't run HTML), plain, policy, the search data, imagesIn, clock, dayRange, telHref.
Next.js helpers (@caltho/site-kit/next)The prepared content, a page by its path, page metadata, Img (the prepared widths), Md (Markdown that can't run HTML), JsonLd, BlockMark (where each block starts, for the preview), previewOrigins. The content is a store: on the server it reads .site/prepared.json; in the preview, what CalOS sends.
Preview (@caltho/site-kit/next/preview)SitePreview: CalOS's draft drawn with the site's own components, a click matched to its field, outlines with each section's name.
Schema (@caltho/site-kit/schema)What CalOS needs of a declaration with no site code: validate content against the JSON Schema with messages a person can act on, make a blank block or entry, list the kinds of block, and the checks across pages.

Sections

Sections any site can use, built once here (accessible, sized for each screen, inside the content security policy) so a site gets them with a kit update rather than code of its own. CalOS offers them in its editor like the site's own, and Cal can add one. Today: gallery (photos in a grid, each with an optional caption, opening its largest copy; 2 to 4 across, square, landscape, portrait or as taken) and slideshow (photos one at a time, fading from one to the next, with captions).

Your own section (own) is built in CalOS rather than in code: a heading and intro over one to four columns (or a wide one beside a narrow one), each a list of parts (heading, text, photo, button, list, quote, facts in a row, price, photos in a grid, slideshow, space, table, icon, logos, video), left or centred, plain or in the brand colour. A column can be drawn as a card (card: true). Its parts wear the site's own classes, so what's built looks like the site.

  • table: 2 to 5 columns (the first over the rows' labels, its heading may be ""), 1 to 20 rows, a column picked out (highlight, counting from 1 after the labels). A cell "yes" or "no" is a tick or a dash, read out as the word. A real table from 640px up; below, each row a small card.
  • icon: one of the kit's line icons by name (ICON_NAMES: check, calendar, clock, users, …), drawn inline in the brand colour, small, medium or large.
  • logos: 2 to 12, in a row in grey, with a line above if it has one.
  • video: a YouTube or Vimeo address and what the video is (its title, for a screen reader), shown in YouTube's no-cookie player or Vimeo's with tracking off. finish lets the player in (frame-src) only on a page that shows one, and in the preview. One that's wanted again is saved by name in the content's saved list, and CalOS offers it beside the site's own kinds.

Slideshow (from @caltho/site-kit/next) is also a building block a site's own sections use: SCV's hero moves through its photos behind its words with <Slideshow fill label="…">{photos}</Slideshow>. It waits while it's off screen, pointed at or focused, never moves for someone who's asked for less motion, and has a button to stop it and one for each photo.

A site takes them in once, three lines:

// content/schema.ts: the kit's sections beside the site's own (one of its own by the same name is kept)
import { sections } from "@caltho/site-kit/sections";
export const blocks = { ...sections, hero, about /* … */ };

// the site's page: every block after its mark, framed when it has a background, the kit drawing
// whatever kind the site's own `render` leaves (returns null for)
<KitBlocks blocks={page.blocks} render={(block, i) => <Block s={block} />} level={(i) => (i === 0 ? 1 : 2)} />

// app/layout.tsx
import "@caltho/site-kit/sections.css";

A section of the business's own has a background (tone: plain, soft, brand or dark, from the Look's --look-*, with --look-page and --look-ink from the Look's background and ink), space above and below (spacing: tight, normal, roomy) and can have a photo behind it (background, under a scrim). On soft, brand or dark it's a card inset within the content's width in the theme's corners (shape: "card", the default) or a band across the page (shape: "band"). On brand, dark or a photo the kit keeps its words and buttons readable over the site's own colour classes. presets (from @caltho/site-kit/sections) are ready-made ones (an opening, words over a photo, three reasons, a client's words, prices, a call to get in touch, contact, common questions, a photo and words, facts in a row, a comparison, features with icons, a logo strip, a video), each an own block to add and edit like any other.

From then on each section the kit adds reaches the site with cal sites kit update. They're drawn from the site's Look (--look-*: its type scale, colours, buttons, width and spacing) and need no classes of the site's; a site may still give its own for any part, in place of the kit's: <KitBlocks … classes={{ headingLarge: "font-display text-3xl" }} />.

The theme

The Look is the site's theme. Beside its fonts, brand and accent colours, corners and buttons, it has the page's colours by role (page, ink, muted, line, soft), colours of the site's own by name (palette), a type scale (type: display, h1, h2, h3, body and small, each a size in px, a weight, tracking in em and a line height), the content's width and the space between sections. A site's look options are its design as it is (the defaults CalOS shows); its CSS reads the variables (--look-page, --look-swatch-sand, --look-type-h2-size, --look-width, --look-section-pad, the full list in src/content/look.ts) so a change in CalOS reaches every page. --look-gutter is the space each side of the content (the site's gutter option in px on a wide screen, 40 unless said, half on a phone): a site's own wrap reads it so its sections and the kit's line up.

The menu edited in CalOS can have links under a link (children, up to 8, one level): menuOf returns them, and the site's header draws them (the starter: a dropdown on a wide screen, indented in the phone's menu). blockLabel(block, kindTitle) (@caltho/site-kit/schema) is what a block is called in a list: its heading, an own section's first heading part, else its kind.

Every section's Layout (placement, on unless a site sets placement: false) has a Background: a colour (a role, or one of the site's own by its slug) and a photo under a shade. KitBlocks draws it as a frame around the section (frameClass), its words light when the colour or photo is dark (backgroundIsDark, from the Look's values at build and in the preview). The schema a site writes carries the ready-made sections its own kind takes, as x-presets at its root.

Share cards

Each page is shared (Open Graph) with a 1200×630 card made at build from its words: its title and description, or the Share title and Share text an owner can set in CalOS's page settings, with the business's name. Every publish rebuilds the site, so a card always says what the page says. A Share picture chosen for a page is used instead.

A site sets how its cards look in content/schema.ts:

export default defineSite(blocks, {
  share: {
    background: "#13262f", color: "#ffffff", muted: "#b8c7cf", accent: "#f2b134",
    backgroundImage: "public/og.png",       // drawn behind, stretched to 1200×630
    logo: "public/logo.svg",                // instead of the business's name in words
    fonts: [{ name: "Brand Sans", path: "assets/fonts/BrandSans-Bold.woff", weight: 700 }],
    prefer: "picture",                      // a page's first picture before its card
    render: (card, h) => h.el({ width: "100%", height: "100%", padding: 72 }, card.title), // a card of its own
  },
});

share: false turns cards off: pages are shared with a picture from them. The kit carries DM Sans (SIL Open Font License, src/fonts/OFL.txt) for cards that don't name fonts of their own.

A build

site-kit prepare     # before: content checked, schema written, pictures ready
next build           # the framework, reading .site/prepared.json
site-kit finish out  # after: security policy, _headers, _redirects
site-kit check out   # then: the browser check

This is the whole contract between a site and CalOS: content in at build, the declaration out, redirects in at build, files out. A site in another framework uses the same prepare and finish and reads .site/prepared.json its own way.

prepare reads content/schema.ts directly, so it needs Node 22.18 or later.

Working on it

bun install
bun run typecheck && bun test src && bun run build

To try a change in a site before releasing it: bun pm pack, then in the site bun add file:/path/to/caltho-site-kit-<version>.tgz.

Releasing: raise version in package.json and merge to main. The Publish workflow sees a version that isn't on npm yet, checks and builds it, and stages it with npm's trusted publishing (no token is stored anywhere). A person then approves it, which is the 2FA step: npm stage list, then npm stage approve <id> (or approve on npmjs.com). Only then is it live, and a site's Dependabot picks it up. Set on the package once: Settings › Trusted publisher › GitHub Actions: organization trycaltho, repository site-kit, workflow publish.yml, permission "stage publish".

版本与发布记录

@caltho/site-kit 共发布 15 个版本,首次发布于 2026/10/25;过去 12 个月发布了 15 个版本。

版本发布日期距今
0.2.11latest2026/11/09今天
0.2.102026/11/027 天前
0.2.92026/11/017 天前
0.2.82026/11/018 天前
0.2.72026/11/018 天前
0.2.62026/10/2713 天前

关于 @caltho/site-kit 的常见问题

@caltho/site-kit 是什么?

What every Caltho site is built with: the contract with CalOS (content, and how a site declares what is editable), images, search and security, and the checks

如何安装 @caltho/site-kit?

在项目目录运行 npm install @caltho/site-kit,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 site-kit,可以用 npx site-kit 直接运行。

@caltho/site-kit 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@caltho/site-kit 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@caltho/site-kit 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@caltho/site-kit 可以商用吗?

@caltho/site-kit 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@caltho/site-kit 的下载量有多少?

根据 npm 官方下载统计,@caltho/site-kit 在 2026/10/09 – 2026/11/07 这 30 天内被下载 3,691 次,日均约 123 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@caltho/site-kit 还在维护吗?

最新版本 0.2.11 发布于 2026/11/09(今天);至今共发布 15 个版本,其中过去 12 个月发布了 15 个;首次发布于 2026/10/25。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。