Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ yarn-error.log*
.env*.local
.vercel
next-env.d.ts
/AGENTS.md
/CLAUDE.md

__pycache__/
.vscode/

api-reference/nodejs-api/index.d.ts

public/api-reference/nodejs/
public/api-reference/python/
public/api-reference/python/
35 changes: 35 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Blog contribution guide

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个文件要不移动到 content/blog 下面,作为编写 blog 的指导说明

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

维护一个contributing.md就可以了吧,放在外面醒目且常用一些,标题暂时是blog contributing guide,后面有其他guide可以再修改标题

Every post must provide a repository-hosted cover image. The blog index uses one predictable presentation rule for every cover, so article-specific fit overrides are not supported.

## Cover requirements

- Store cover assets under `public/img/blog/<post-slug>/` and reference them with `/img/blog/<post-slug>/<file>`.
- Use a 16:9 landscape canvas. Recommended export size: `1600x900` or `1920x1080`; minimum accepted size: `800x450`.
- Keep titles, logos, diagrams, and other essential content inside a 7% safe margin. Covers are displayed with `object-fit: cover` and may lose a very small edge area because of responsive rounding.
- Prefer providing both `image` and `imageDark`, designed for light and dark page backgrounds respectively. Both covers must use the same dimensions, composition, and safe-area rules.
- Do not use remote image URLs or add per-post CSS/object-fit exceptions.

## Frontmatter

```yaml
---
title: "Post title"
date: "YYYY-MM-DD"
image: /img/blog/YYYY-MM-DD-post-slug/header.webp
imageDark: /img/blog/YYYY-MM-DD-post-slug/header-dark.webp # optional
description: "Short summary"
---
```

## Before opening a PR

Run:

```bash
npm run blog:covers:check
npm run types:check
npm run build
```

`npm run build` also runs the cover check. A blog PR with a missing, remote, undersized, or incorrectly proportioned cover will fail CI before deployment.
2 changes: 2 additions & 0 deletions app/[lang]/(home)/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,15 @@ function BlogCover({
src={image}
alt={title}
loading={loading}
decoding="async"
/>
{imageDark && (
<img
className="hidden dark:block"
src={imageDark}
alt=""
loading={loading}
decoding="async"
aria-hidden="true"
/>
)}
Expand Down
11 changes: 9 additions & 2 deletions app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -6218,6 +6218,7 @@ body:has(.zvec-docs-overview) #nd-sidebar > div:nth-child(2) {


.zvec-blog-featured-media {
position: relative;
min-height: 330px;
overflow: hidden;
border-right: 1px solid var(--zvec-border);
Expand All @@ -6226,9 +6227,12 @@ body:has(.zvec-docs-overview) #nd-sidebar > div:nth-child(2) {


.zvec-blog-featured-media img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
object-fit: cover;
object-position: center;
transition: transform 240ms ease;
}

Expand Down Expand Up @@ -6331,9 +6335,12 @@ body:has(.zvec-docs-overview) #nd-sidebar > div:nth-child(2) {


.zvec-blog-card-media img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
object-fit: cover;
object-position: center;
transition: transform 240ms ease;
}

Expand Down
1 change: 1 addition & 0 deletions content/blog/en/2026-09-08-zvec-rabitq.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
title: "RaBitQ in Zvec: Quantization Principles, Index Design, and Benchmarks"
date: "2026-09-08"
image: /img/blog/2026-09-08-zvec-rabitq/header.png
imageDark: /img/blog/2026-09-08-zvec-rabitq/header-dark.jpg
description: "Explore native HNSW-RaBitQ and IVF-RaBitQ in Zvec through benchmarks, usage examples, randomized vector quantization, error bounds, and index design."
---

Expand Down
1 change: 1 addition & 0 deletions content/blog/en/2026-09-17-zvec-multi-platform.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
title: "Making Vector Search Available Everywhere: A Complete View of Zvec Multi-Platform Support"
date: "2026-09-17"
image: /img/blog/2026-09-17-zvec-multi-platform/header.webp
imageDark: /img/blog/2026-09-17-zvec-multi-platform/header-dark.jpg
description: "Zvec runs wherever your application runs: Linux, macOS, Windows, Android, and iOS, with official SDKs for Python, Node.js, Go, Rust, Java, and Dart/Flutter plus ready-to-use prebuilt binaries. A complete inventory of CI-verified platforms, per-platform index availability, and the cross-platform engineering foundation behind them."
---

Expand Down
1 change: 1 addition & 0 deletions content/blog/zh/2026-09-08-zvec-rabitq.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
title: "RaBitQ 落地 Zvec:量化原理、索引设计与性能实测"
date: "2026-09-08"
image: /img/blog/2026-09-08-zvec-rabitq/header.png
imageDark: /img/blog/2026-09-08-zvec-rabitq/header-dark.jpg
description: "Zvec 原生集成 HNSW-RaBitQ 与 IVF-RaBitQ:从性能实测和使用示例,理解随机化向量量化、误差界与索引实现。"
---

Expand Down
1 change: 1 addition & 0 deletions content/blog/zh/2026-09-17-zvec-multi-platform.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
title: "让向量检索随处可用:Zvec 多平台支持全景"
date: "2026-09-17"
image: /img/blog/2026-09-17-zvec-multi-platform/header.webp
imageDark: /img/blog/2026-09-17-zvec-multi-platform/header-dark.jpg
description: "Zvec 已支持 Linux、macOS、Windows、Android 与 iOS,提供 Python、Node.js、Go、Rust、Java、Dart/Flutter 六种官方 SDK 与开箱即用的预编译产物。本文完整盘点 CI 验证的平台、各平台索引可用性、不同内存预算下的选型思路,以及背后的跨平台工程底座。"
---

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"version": "0.0.0",
"private": true,
"scripts": {
"build": "next build",
"build": "npm run blog:covers:check && next build",
"blog:covers:check": "node scripts/validate-blog-covers.mjs",
"dev": "next dev",
"start": "next start",
"types:check": "fumadocs-mdx && tsc --noEmit",
Expand Down
Binary file modified public/img/blog/2026-01-20-introduction/header.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/img/blog/2026-06-22-zvec-mobile/cover.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/img/blog/2026-09-08-zvec-rabitq/header.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
166 changes: 166 additions & 0 deletions scripts/validate-blog-covers.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { readFile, readdir } from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';

const ROOT = process.cwd();
const BLOG_ROOT = path.join(ROOT, 'content', 'blog');
const PUBLIC_ROOT = path.join(ROOT, 'public');
const TARGET_RATIO = 16 / 9;
const MAX_RATIO_ERROR = 0.02;
const MIN_WIDTH = 800;
const MIN_HEIGHT = 450;

async function walk(directory) {
const entries = await readdir(directory, { withFileTypes: true });
const files = await Promise.all(entries.map(async (entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? walk(target) : [target];
}));
return files.flat();
}

function readFrontmatter(source) {
if (!source.startsWith('---')) return {};
const end = source.indexOf('\n---', 3);
if (end === -1) return {};

const fields = {};
for (const line of source.slice(3, end).split('\n')) {
const match = line.match(/^([A-Za-z][A-Za-z0-9]*):\s*(.*?)\s*$/);
if (!match) continue;
fields[match[1]] = match[2].replace(/^(['"])(.*)\1$/, '$2');
}
return fields;
}

function pngSize(buffer) {
if (buffer.subarray(1, 4).toString() !== 'PNG') return null;
return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
}

function jpegSize(buffer) {
if (buffer[0] !== 0xff || buffer[1] !== 0xd8) return null;
let offset = 2;

while (offset + 9 < buffer.length) {
if (buffer[offset] !== 0xff) {
offset += 1;
continue;
}

const marker = buffer[offset + 1];
offset += 2;
if (marker === 0xd8 || marker === 0xd9) continue;
const length = buffer.readUInt16BE(offset);
if ([0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf].includes(marker)) {
return { width: buffer.readUInt16BE(offset + 5), height: buffer.readUInt16BE(offset + 3) };
}
offset += length;
}
return null;
}

function webpSize(buffer) {
if (buffer.subarray(0, 4).toString() !== 'RIFF' || buffer.subarray(8, 12).toString() !== 'WEBP') return null;
let offset = 12;

while (offset + 8 <= buffer.length) {
const type = buffer.subarray(offset, offset + 4).toString();
const length = buffer.readUInt32LE(offset + 4);
const data = offset + 8;

if (type === 'VP8X' && data + 10 <= buffer.length) {
return {
width: 1 + buffer.readUIntLE(data + 4, 3),
height: 1 + buffer.readUIntLE(data + 7, 3),
};
}
if (type === 'VP8 ' && data + 10 <= buffer.length) {
return {
width: buffer.readUInt16LE(data + 6) & 0x3fff,
height: buffer.readUInt16LE(data + 8) & 0x3fff,
};
}
if (type === 'VP8L' && data + 5 <= buffer.length && buffer[data] === 0x2f) {
const bits = buffer.readUInt32LE(data + 1);
return {
width: 1 + (bits & 0x3fff),
height: 1 + ((bits >> 14) & 0x3fff),
};
}

offset = data + length + (length % 2);
}
return null;
}

function svgSize(buffer) {
const source = buffer.toString('utf8', 0, Math.min(buffer.length, 4096));
if (!/<svg\b/i.test(source)) return null;
const viewBox = source.match(/viewBox=["']\s*[-\d.]+\s+[-\d.]+\s+([\d.]+)\s+([\d.]+)\s*["']/i);
if (viewBox) return { width: Number(viewBox[1]), height: Number(viewBox[2]) };
const width = source.match(/\bwidth=["']([\d.]+)/i);
const height = source.match(/\bheight=["']([\d.]+)/i);
return width && height ? { width: Number(width[1]), height: Number(height[1]) } : null;
}

function imageSize(buffer) {
return pngSize(buffer) || jpegSize(buffer) || webpSize(buffer) || svgSize(buffer);
}

const contentFiles = (await walk(BLOG_ROOT)).filter((file) => file.endsWith('.mdx'));
const references = new Map();
const errors = [];

for (const contentFile of contentFiles) {
const fields = readFrontmatter(await readFile(contentFile, 'utf8'));
for (const key of ['image', 'imageDark']) {
const image = fields[key];
if (!image) continue;
if (!image.startsWith('/img/blog/')) {
errors.push(`${path.relative(ROOT, contentFile)}: ${key} must be a local /img/blog/... path`);
continue;
}
const owners = references.get(image) || [];
owners.push(`${path.relative(ROOT, contentFile)} (${key})`);
references.set(image, owners);
}
}

for (const [image, owners] of references) {
const asset = path.resolve(PUBLIC_ROOT, `.${image}`);
if (!asset.startsWith(`${PUBLIC_ROOT}${path.sep}`)) {
errors.push(`${image}: path escapes public/`);
continue;
}

let buffer;
try {
buffer = await readFile(asset);
} catch {
errors.push(`${image}: file does not exist (used by ${owners.join(', ')})`);
continue;
}

const size = imageSize(buffer);
if (!size || !Number.isFinite(size.width) || !Number.isFinite(size.height)) {
errors.push(`${image}: unsupported image or unreadable dimensions`);
continue;
}

const ratioError = Math.abs(size.width / size.height - TARGET_RATIO) / TARGET_RATIO;
if (ratioError > MAX_RATIO_ERROR) {
errors.push(`${image}: ${size.width}x${size.height} is not 16:9 (maximum deviation: 2%)`);
}
if (size.width < MIN_WIDTH || size.height < MIN_HEIGHT) {
errors.push(`${image}: ${size.width}x${size.height} is smaller than ${MIN_WIDTH}x${MIN_HEIGHT}`);
}
}

if (errors.length > 0) {
console.error('Blog cover validation failed:\n');
for (const error of errors) console.error(`- ${error}`);
process.exit(1);
}

console.log(`Validated ${references.size} blog cover assets (16:9, local, minimum ${MIN_WIDTH}x${MIN_HEIGHT}).`);
Loading