Skip to content

Commit 9ccdb68

Browse files
authored
Merge pull request #171 from maxrjones/migrate-to-vitepress
2 parents 70f4a32 + 837e2c5 commit 9ccdb68

200 files changed

Lines changed: 2939 additions & 31144 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/deploy.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Deploy VitePress site to Pages
2+
3+
on:
4+
push:
5+
branches: [main]
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
11+
concurrency:
12+
group: pages
13+
cancel-in-progress: false
14+
15+
jobs:
16+
build:
17+
runs-on: ubuntu-latest
18+
permissions:
19+
contents: read
20+
pages: write
21+
steps:
22+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
23+
with:
24+
fetch-depth: 0
25+
persist-credentials: false
26+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
27+
with:
28+
node-version: 20
29+
cache: npm
30+
- uses: actions/configure-pages@1f0c5cde4bc74cd7e1254d0cb4de8d49e9068c7d # v4
31+
- run: npm ci
32+
- run: npm run docs:build
33+
- uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
34+
with:
35+
path: .vitepress/dist
36+
37+
deploy:
38+
needs: build
39+
runs-on: ubuntu-latest
40+
permissions:
41+
pages: write
42+
id-token: write
43+
environment:
44+
name: github-pages
45+
url: ${{ steps.deployment.outputs.page_url }}
46+
steps:
47+
- id: deployment
48+
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4

.gitignore

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
_site
2-
.sass-cache
3-
.jekyll-metadata
1+
node_modules/
2+
.vitepress/dist/
3+
.vitepress/cache/
44
.DS_Store
5-
.swp
6-
*~

.readthedocs.yml

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
1-
# .readthedocs.yml
21
# Read the Docs configuration file
3-
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
2+
# See https://docs.readthedocs.com/platform/stable/build-customization.html
3+
# RTD configures the VitePress base URL automatically at build time.
44

5-
# Required
65
version: 2
76

8-
# Set the version of Python and other tools you might need
97
build:
10-
os: ubuntu-24.04
11-
8+
os: ubuntu-lts-latest
129
tools:
13-
ruby: "3.4"
14-
15-
commands:
16-
- bundle install
17-
- >
18-
JEKYLL_ENV=production bundle exec jekyll build --destination
19-
_readthedocs/html --baseurl $(echo -n "$READTHEDOCS_CANONICAL_URL" | cut
20-
-d '/' -f 4-)
10+
nodejs: "latest"
11+
jobs:
12+
install:
13+
- npm ci
14+
build:
15+
html:
16+
- npm run docs:build
17+
- mkdir -p $READTHEDOCS_OUTPUT/
18+
- mv .vitepress/dist $READTHEDOCS_OUTPUT/html

.vitepress/config.mts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { defineConfig } from 'vitepress'
2+
import { redirectsPlugin, defaultPluginOptions } from './plugins/redirects'
3+
4+
// Read the path prefix from RTD's canonical URL env var when present
5+
// (e.g. https://<slug>.readthedocs.build/en/171/ → /en/171/), so all
6+
// asset URLs resolve correctly under the PR-preview subpath. Defaults
7+
// to '/' for the GitHub Pages build at zarr.dev.
8+
const rtdUrl = process.env.READTHEDOCS_CANONICAL_URL
9+
const base = rtdUrl ? new URL(rtdUrl).pathname : '/'
10+
11+
export default defineConfig({
12+
base,
13+
title: 'Zarr',
14+
description: 'Zarr is a community project to develop specifications and software for storage of large N-dimensional typed arrays, also commonly known as tensors.',
15+
cleanUrls: true,
16+
lastUpdated: true,
17+
sitemap: { hostname: 'https://zarr.dev' },
18+
head: [
19+
['link', { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
20+
['link', { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16.png' }],
21+
['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png' }],
22+
['link', { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' }],
23+
['link', { rel: 'manifest', href: '/site.webmanifest' }],
24+
['link', { rel: 'mask-icon', href: '/safari-pinned-tab.svg', color: '#5bbad5' }],
25+
['meta', { name: 'apple-mobile-web-app-title', content: 'Zarr' }],
26+
['meta', { name: 'application-name', content: 'Zarr' }],
27+
['meta', { name: 'theme-color', content: '#ffffff' }],
28+
['meta', { property: 'og:type', content: 'website' }],
29+
['meta', { property: 'og:site_name', content: 'Zarr' }],
30+
['meta', { property: 'og:title', content: 'Zarr' }],
31+
['meta', { property: 'og:description', content: 'An open, community-driven format for storing large arrays in any key-value store, including cloud object storage.' }],
32+
['meta', { property: 'og:image', content: 'https://zarr.dev/android-chrome-512x512.png' }],
33+
['meta', { property: 'og:url', content: 'https://zarr.dev/' }],
34+
['meta', { name: 'twitter:card', content: 'summary' }],
35+
['meta', { name: 'twitter:title', content: 'Zarr' }],
36+
['meta', { name: 'twitter:description', content: 'An open, community-driven format for storing large arrays in any key-value store, including cloud object storage.' }],
37+
['meta', { name: 'twitter:image', content: 'https://zarr.dev/android-chrome-512x512.png' }],
38+
['script', { async: '', src: 'https://www.googletagmanager.com/gtag/js?id=G-BCRR9QE7Z0' }],
39+
['script', {}, `window.dataLayer = window.dataLayer || [];
40+
function gtag(){dataLayer.push(arguments);}
41+
gtag('js', new Date());
42+
gtag('config', 'G-BCRR9QE7Z0');`],
43+
],
44+
srcExclude: [
45+
'docs/**',
46+
'README.md',
47+
'NOTICE.md',
48+
],
49+
vite: {
50+
plugins: [redirectsPlugin(defaultPluginOptions(process.cwd()))],
51+
},
52+
themeConfig: {
53+
logo: '/android-chrome-192x192.png',
54+
search: { provider: 'local' },
55+
nav: [
56+
{ text: 'Adopters', link: '/adopters' },
57+
{ text: 'Community', link: '/community' },
58+
{ text: 'Conventions', link: '/conventions' },
59+
{ text: 'Datasets', link: '/datasets' },
60+
{ text: 'Implementations', link: '/implementations' },
61+
{ text: 'Office Hours', link: '/office-hours' },
62+
{ text: 'Slides', link: '/slides' },
63+
{
64+
text: 'External',
65+
items: [
66+
{ text: 'Blog', link: 'https://zarr.dev/blog/' },
67+
{ text: 'Specification', link: 'https://zarr-specs.readthedocs.io/' },
68+
{ text: 'ZEPs', link: 'https://zarr.dev/zeps/' },
69+
{ text: 'Documentation', link: 'https://zarr.readthedocs.io/en/stable/' },
70+
],
71+
},
72+
],
73+
socialLinks: [
74+
{
75+
icon: {
76+
svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 530"><path d="M135.72 44.03C202.216 93.951 273.74 195.17 300 249.49c26.262-54.316 97.782-155.54 164.28-205.46C512.26 8.009 590-19.862 590 68.825c0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.38-3.69-10.832-3.708-7.896-.017-2.936-1.193.516-3.707 7.896-13.714 40.255-67.233 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.45-163.25-81.433C20.15 217.612 9.997 86.535 9.997 68.825c0-88.687 77.742-60.816 125.72-24.795z" fill="currentColor"/></svg>',
77+
},
78+
link: 'https://bsky.app/profile/zarr.dev',
79+
ariaLabel: 'Bluesky',
80+
},
81+
{ icon: 'mastodon', link: 'https://fosstodon.org/@zarr' },
82+
{ icon: 'github', link: 'https://github.com/zarr-developers' },
83+
{
84+
icon: {
85+
svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12c0 1.81.49 3.51 1.34 4.97L2 22l5.16-1.35A9.95 9.95 0 0 0 12 22c5.52 0 10-4.48 10-10S17.52 2 12 2z" fill="currentColor"/></svg>',
86+
},
87+
link: 'https://ossci.zulipchat.com/',
88+
ariaLabel: 'Zulip',
89+
},
90+
],
91+
editLink: {
92+
pattern: 'https://github.com/zarr-developers/zarr-developers.github.io/edit/main/:path',
93+
text: 'Edit this page on GitHub',
94+
},
95+
footer: {
96+
message: 'Released under the MIT License.',
97+
copyright: 'Copyright © Zarr contributors',
98+
},
99+
},
100+
})

.vitepress/plugins/redirects.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { readFileSync, mkdirSync, writeFileSync, existsSync } from 'node:fs'
2+
import { dirname, join, resolve } from 'node:path'
3+
import yaml from 'js-yaml'
4+
import type { Plugin } from 'vite'
5+
6+
interface RedirectEntry {
7+
from: string
8+
to: string
9+
}
10+
11+
const escapeHtml = (s: string): string =>
12+
s.replace(/&/g, '&amp;')
13+
.replace(/"/g, '&quot;')
14+
.replace(/</g, '&lt;')
15+
.replace(/>/g, '&gt;')
16+
17+
const renderRedirectHtml = (to: string): string => {
18+
const target = escapeHtml(to)
19+
return `<!doctype html>
20+
<html lang="en">
21+
<head>
22+
<meta charset="utf-8">
23+
<title>Redirecting...</title>
24+
<link rel="canonical" href="${target}">
25+
<meta http-equiv="refresh" content="0; url=${target}">
26+
<meta name="robots" content="noindex">
27+
</head>
28+
<body>
29+
<p>Redirecting to <a href="${target}">${target}</a>...</p>
30+
<script>location.replace("${target}")</script>
31+
</body>
32+
</html>
33+
`
34+
}
35+
36+
const parseRedirectsYaml = (source: string): RedirectEntry[] => {
37+
const parsed = yaml.load(source)
38+
if (parsed === null || parsed === undefined) return []
39+
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
40+
throw new Error('redirects.yml must be a YAML mapping at the top level')
41+
}
42+
const entries: RedirectEntry[] = []
43+
for (const [from, to] of Object.entries(parsed as Record<string, unknown>)) {
44+
if (typeof to !== 'string') {
45+
throw new Error(`redirect target for "${from}" must be a string, got ${typeof to}`)
46+
}
47+
if (!from.startsWith('/')) {
48+
throw new Error(`redirect source "${from}" must start with a leading slash`)
49+
}
50+
entries.push({ from, to })
51+
}
52+
return entries
53+
}
54+
55+
const stripLeadingSlash = (p: string): string => (p.startsWith('/') ? p.slice(1) : p)
56+
const stripTrailingSlash = (p: string): string => (p.endsWith('/') ? p.slice(0, -1) : p)
57+
58+
interface RedirectsPluginOptions {
59+
yamlPath: string
60+
outDir: string
61+
}
62+
63+
export const redirectsPlugin = (options: RedirectsPluginOptions): Plugin => {
64+
// VitePress emits both SSR and client bundles, so closeBundle fires twice.
65+
// Only emit stubs on the first call.
66+
let written = false
67+
return {
68+
name: 'zarr-redirects',
69+
apply: 'build',
70+
closeBundle() {
71+
if (written) return
72+
written = true
73+
74+
const yamlSource = readFileSync(options.yamlPath, 'utf8')
75+
const entries = parseRedirectsYaml(yamlSource)
76+
77+
for (const { from, to } of entries) {
78+
const cleaned = stripTrailingSlash(stripLeadingSlash(from))
79+
const outPath = cleaned === ''
80+
? join(options.outDir, 'index.html')
81+
: join(options.outDir, cleaned, 'index.html')
82+
83+
if (existsSync(outPath)) {
84+
throw new Error(
85+
`Redirect "${from}" collides with an existing build output at ${outPath}. ` +
86+
`Either remove the redirect or rename the page that produced ${outPath}.`,
87+
)
88+
}
89+
90+
mkdirSync(dirname(outPath), { recursive: true })
91+
writeFileSync(outPath, renderRedirectHtml(to), 'utf8')
92+
}
93+
94+
console.log(`[zarr-redirects] wrote ${entries.length} redirect stubs`)
95+
},
96+
}
97+
}
98+
99+
export const defaultPluginOptions = (rootDir: string): RedirectsPluginOptions => ({
100+
yamlPath: resolve(rootDir, '.vitepress/redirects.yml'),
101+
outDir: resolve(rootDir, '.vitepress/dist'),
102+
})

.vitepress/redirects.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Each entry maps a path on zarr.dev to an absolute or relative target URL.
2+
# A buildEnd hook turns this into static meta-refresh stub HTML files
3+
# under .vitepress/dist/<from>/index.html.
4+
5+
# numcodecs codec docs (mirrors the deleted numcodecs_redirects/ verbatim)
6+
/numcodecs/adler32: https://numcodecs.readthedocs.io/en/stable/checksum32.html#adler32
7+
/numcodecs/astype: https://numcodecs.readthedocs.io/en/stable/astype.html
8+
/numcodecs/bitround: https://numcodecs.readthedocs.io/en/stable/bitround.html
9+
/numcodecs/blosc: https://numcodecs.readthedocs.io/en/stable/blosc.html
10+
/numcodecs/bz2: https://numcodecs.readthedocs.io/en/stable/bz2.html
11+
/numcodecs/crc32: https://numcodecs.readthedocs.io/en/stable/checksum32.html#crc32
12+
/numcodecs/delta: https://numcodecs.readthedocs.io/en/stable/delta.html
13+
/numcodecs/fixedscaleoffset: https://numcodecs.readthedocs.io/en/stable/fixedscaleoffset.html
14+
/numcodecs/fletcher32: https://numcodecs.readthedocs.io/en/stable/checksum32.html#fletcher32
15+
/numcodecs/gzip: https://numcodecs.readthedocs.io/en/stable/gzip.html
16+
/numcodecs/jenkins_lookup3: https://numcodecs.readthedocs.io/en/stable/checksum32.html#jenkinslookup3
17+
/numcodecs/lz4: https://numcodecs.readthedocs.io/en/stable/lz4.html
18+
/numcodecs/lzma: https://numcodecs.readthedocs.io/en/stable/lzma.html
19+
/numcodecs: https://numcodecs.readthedocs.io/en/stable/
20+
/numcodecs/packbits: https://numcodecs.readthedocs.io/en/stable/packbits.html
21+
/numcodecs/pcodec: https://numcodecs.readthedocs.io/en/stable/pcodec.html
22+
/numcodecs/quantize: https://numcodecs.readthedocs.io/en/stable/quantize.html
23+
/numcodecs/shuffle: https://numcodecs.readthedocs.io/en/stable/shuffle.html
24+
/numcodecs/zfpy: https://numcodecs.readthedocs.io/en/stable/zfpy.html
25+
/numcodecs/zlib: https://numcodecs.readthedocs.io/en/stable/zlib.html
26+
/numcodecs/zstd: https://numcodecs.readthedocs.io/en/stable/zstd.html
27+
28+
# URL hygiene: catch any external link to /office_hours/
29+
/office_hours/: /office-hours/
30+
31+
# Restored behavior of the deleted about/index.html stub
32+
/about: /
33+
34+
# Vestigial 2019 blog posts -> blog repo
35+
/2019/05/02/zarr-2.3-release/: https://zarr.dev/blog/release-23/
36+
/2019/06/19/zarr-v3-update/: https://zarr.dev/blog/v3-update/

0 commit comments

Comments
 (0)