@anja.codes/create-vitakit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -5
- package/deployments.mjs +178 -0
- package/index.mjs +36 -76
- package/package.json +4 -2
- package/scaffold.mjs +124 -0
- package/template/_forgejo/workflows/codeberg.yml +37 -0
- package/template/_github/workflows/cloudflare.yml +6 -4
- package/template/_github/workflows/cv.yml +1 -1
- package/template/_github/workflows/workers.yml +39 -0
- package/template/app/assets/css/main.css +12 -0
- package/template/app/pages/cv.vue +13 -18
- package/template/app/pages/index.vue +4 -4
- package/template/app/pages/publications.vue +6 -4
- package/template/content/pages/cv.md +1 -1
- package/template/content/pages/index.md +2 -2
- package/template/content/profile.yml +5 -6
- package/template/content/publications.bib +1 -1
- package/template/cv-tex/cv.tex +8 -7
- package/template/cv-tex/latexmkrc +1 -1
- package/template/cv-tex/preamble.tex +3 -6
- package/template/gitlab-ci.yml.template +28 -0
- package/template/package.json.template +3 -4
- package/template/wrangler.json.template +10 -0
package/README.md
CHANGED
|
@@ -8,16 +8,41 @@ pnpm create @anja.codes/vitakit my-site
|
|
|
8
8
|
cd my-site
|
|
9
9
|
pnpm install
|
|
10
10
|
pnpm dev # http://localhost:3000
|
|
11
|
-
pnpm
|
|
11
|
+
pnpm pdf # cv-tex/cv.pdf (needs a TeX distribution)
|
|
12
12
|
pnpm check # what CI runs
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
`npm create @anja.codes/vitakit my-site` works too: the project is then written
|
|
16
|
+
for npm, with `npm run …` in its scripts, workflows and instructions.
|
|
17
|
+
|
|
15
18
|
It asks for your name, position, affiliation, email and a deployment target
|
|
16
|
-
(GitHub Pages, Cloudflare
|
|
19
|
+
(GitHub Pages, Cloudflare Workers or Pages, GitLab Pages, Codeberg Pages,
|
|
20
|
+
existing web space via SSH/rsync, or decide later), copies the template and
|
|
17
21
|
substitutes those few values.
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
Choose `?` at the deployment prompt for a compact comparison of URLs,
|
|
24
|
+
requirements and CI. Numbered setup steps follow your selection.
|
|
25
|
+
To read the comparison without creating files:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pnpm create @anja.codes/vitakit --deployment-help
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- **GitHub Pages:** public source on Free; a paid plan for private source.
|
|
32
|
+
- **Cloudflare Workers:** recommended for new Cloudflare projects; uses GitHub Actions.
|
|
33
|
+
- **Cloudflare Pages:** legacy option, still available; also uses GitHub Actions.
|
|
34
|
+
- **GitLab Pages:** private source supported on Free; uses GitLab CI/CD.
|
|
35
|
+
- **Codeberg Pages:** uses git-pages and Forgejo Actions. Hosted CI requires public,
|
|
36
|
+
freely licensed source; use your own runner for sensitive CV source.
|
|
37
|
+
- **SSH/rsync:** needs existing web space; the preset uses Forgejo Actions.
|
|
38
|
+
|
|
39
|
+
Choosing to decide later adds no workflows and leaves all hosting options open.
|
|
40
|
+
|
|
41
|
+
See [publishing and setup](../docs/guide/publishing.md) for details.
|
|
42
|
+
|
|
43
|
+
Everything afterwards is ordinary files in a repository you own. The
|
|
44
|
+
`vitakit` command in the project helps with the common changes — `pnpm vitakit
|
|
45
|
+
status` shows what is there, `pnpm vitakit add section <name>` sets up a new
|
|
46
|
+
section — and shows every change before it makes it.
|
|
22
47
|
|
|
23
48
|
MIT
|
package/deployments.mjs
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/** Hosting choices, comparison facts and actionable setup steps. */
|
|
2
|
+
export const DEPLOYMENTS = {
|
|
3
|
+
'github-pages': {
|
|
4
|
+
label: 'GitHub Pages',
|
|
5
|
+
description: 'Static hosting built into GitHub.',
|
|
6
|
+
details: {
|
|
7
|
+
URL: 'https://<user>.github.io/<repo>/ or a custom domain.',
|
|
8
|
+
Source: 'Public on Free; private needs GitHub Pro (personal) or Team/Enterprise (org).',
|
|
9
|
+
CI: 'GitHub Actions.',
|
|
10
|
+
},
|
|
11
|
+
setup: [
|
|
12
|
+
'In GitHub Settings → Pages, select GitHub Actions as the source.',
|
|
13
|
+
'Check .github/workflows/deploy.yml:\n- Project site: keep NUXT_APP_BASE_URL=/<repo>/.\n- User site (<user>.github.io) or root custom domain: remove that setting.',
|
|
14
|
+
'Commit pnpm-lock.yaml and push to main.',
|
|
15
|
+
],
|
|
16
|
+
notes: ['Private source needs GitHub Pro (personal) or Team/Enterprise (org).'],
|
|
17
|
+
files: ['.github/workflows/deploy.yml', '.github/workflows/cv.yml'],
|
|
18
|
+
},
|
|
19
|
+
'cloudflare-workers': {
|
|
20
|
+
label: 'Cloudflare Workers (recommended)',
|
|
21
|
+
description: 'Cloudflare’s current platform, serving static files without server code.',
|
|
22
|
+
details: {
|
|
23
|
+
URL: 'https://<worker>.<account>.workers.dev/ or a custom domain.',
|
|
24
|
+
Source: 'Private repos supported on Free; hosting/CI limits apply.',
|
|
25
|
+
CI: 'GitHub Actions.',
|
|
26
|
+
},
|
|
27
|
+
setup: [
|
|
28
|
+
'Enable a workers.dev subdomain in your Cloudflare account.',
|
|
29
|
+
'Set the Worker name in wrangler.json.',
|
|
30
|
+
'Add GitHub Actions repository secrets:\n- CLOUDFLARE_ACCOUNT_ID\n- CLOUDFLARE_API_TOKEN (Workers Scripts: Edit permission)',
|
|
31
|
+
'Commit pnpm-lock.yaml and push to main.',
|
|
32
|
+
],
|
|
33
|
+
files: ['.github/workflows/workers.yml', '.github/workflows/cv.yml', 'wrangler.json'],
|
|
34
|
+
},
|
|
35
|
+
'cloudflare': {
|
|
36
|
+
label: 'Cloudflare Pages (legacy)',
|
|
37
|
+
description: 'Legacy Cloudflare option. Still available; use Workers for new projects.',
|
|
38
|
+
details: {
|
|
39
|
+
URL: 'https://<project>.pages.dev/ or a custom domain.',
|
|
40
|
+
Source: 'Private repos supported on Free; hosting/CI limits apply.',
|
|
41
|
+
CI: 'GitHub Actions, uploading a prebuilt site (Direct Upload).',
|
|
42
|
+
},
|
|
43
|
+
setup: [
|
|
44
|
+
'Create a Cloudflare Pages Direct Upload project; production branch: main.',
|
|
45
|
+
'Set --project-name in .github/workflows/cloudflare.yml to match.',
|
|
46
|
+
'Add GitHub Actions repository secrets:\n- CLOUDFLARE_ACCOUNT_ID\n- CLOUDFLARE_API_TOKEN (Cloudflare Pages: Edit permission)',
|
|
47
|
+
'Commit pnpm-lock.yaml and push to main.',
|
|
48
|
+
],
|
|
49
|
+
files: ['.github/workflows/cloudflare.yml', '.github/workflows/cv.yml'],
|
|
50
|
+
},
|
|
51
|
+
'gitlab-pages': {
|
|
52
|
+
label: 'GitLab Pages',
|
|
53
|
+
description: 'Static hosting on GitLab.com or a university’s GitLab instance.',
|
|
54
|
+
details: {
|
|
55
|
+
URL: 'https://<project>-<id>.gitlab.io/; project paths/custom domains also work.',
|
|
56
|
+
Source: 'Private repos supported on Free; CI quotas apply.',
|
|
57
|
+
CI: 'GitLab CI/CD. Self-managed instances need GitLab 17.10+.',
|
|
58
|
+
},
|
|
59
|
+
setup: [
|
|
60
|
+
'Enable CI runners and Pages in your GitLab project.',
|
|
61
|
+
'For a public site, set Pages access to Everyone in project settings.',
|
|
62
|
+
'Check .gitlab-ci.yml: the base path comes from CI_PAGES_URL.\nFor a root custom domain, set CI/CD variable NUXT_APP_BASE_URL=/.',
|
|
63
|
+
'Commit pnpm-lock.yaml and push to the default branch.',
|
|
64
|
+
],
|
|
65
|
+
files: ['.gitlab-ci.yml'],
|
|
66
|
+
},
|
|
67
|
+
'codeberg-pages': {
|
|
68
|
+
label: 'Codeberg Pages (git-pages)',
|
|
69
|
+
description: 'Static hosting on Codeberg, powered by git-pages.',
|
|
70
|
+
details: {
|
|
71
|
+
URL: 'https://<owner>.codeberg.page/<repo>/ or a custom domain.',
|
|
72
|
+
Source: 'Hosted CI needs public, freely licensed source; private needs your own runner.',
|
|
73
|
+
CI: 'Forgejo Actions, using the automatic token; no publishing branch needed.',
|
|
74
|
+
},
|
|
75
|
+
setup: [
|
|
76
|
+
'Enable Actions in your Codeberg repository.',
|
|
77
|
+
'Choose a Docker-capable runner in .forgejo/workflows/codeberg.yml.\nUse your own runner for private source.',
|
|
78
|
+
'Check site and NUXT_APP_BASE_URL in that workflow.\nFor root/custom domains, update both; custom domains also need DNS authorization.',
|
|
79
|
+
'Commit pnpm-lock.yaml and push to main.',
|
|
80
|
+
],
|
|
81
|
+
notes: ['Codeberg-hosted runners require a public, freely licensed repository.'],
|
|
82
|
+
files: ['.forgejo/workflows/codeberg.yml'],
|
|
83
|
+
},
|
|
84
|
+
'rsync': {
|
|
85
|
+
label: 'Existing web space (SSH/rsync)',
|
|
86
|
+
description: 'Upload to university web space or a server you already control.',
|
|
87
|
+
details: {
|
|
88
|
+
URL: 'Your server’s URL, e.g. https://www.example.edu/~username/.',
|
|
89
|
+
Needs: 'SSH/rsync access and a writable web directory; not SFTP-only hosting.',
|
|
90
|
+
CI: 'Forgejo Actions, not GitHub Actions.',
|
|
91
|
+
},
|
|
92
|
+
setup: [
|
|
93
|
+
'Open .forgejo/workflows/deploy.yml.',
|
|
94
|
+
'Set the SSH user, host, destination path and NUXT_APP_BASE_URL.',
|
|
95
|
+
'Add SSH_DEPLOY_KEY as a Forgejo Actions repository secret.',
|
|
96
|
+
'Configure SSH host-key verification; the template currently disables it.',
|
|
97
|
+
'Commit pnpm-lock.yaml and push to main.',
|
|
98
|
+
],
|
|
99
|
+
notes: ['Check the destination: rsync --delete removes files absent from the build.'],
|
|
100
|
+
files: ['.forgejo/workflows/deploy.yml'],
|
|
101
|
+
},
|
|
102
|
+
'none': {
|
|
103
|
+
label: 'Decide later / deploy manually',
|
|
104
|
+
description: 'Choose a host later; nothing is published.',
|
|
105
|
+
details: {
|
|
106
|
+
Local: 'Website and LaTeX CV work as usual.',
|
|
107
|
+
CI: 'No deployment or PDF-build workflows added.',
|
|
108
|
+
},
|
|
109
|
+
setup: [
|
|
110
|
+
'When ready to publish, run pnpm check.',
|
|
111
|
+
'Upload only .output/public to a static host, not the source or cv-tex/.',
|
|
112
|
+
],
|
|
113
|
+
files: [],
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Wrap CLI prose with hanging indents, preserving explicit sublists. */
|
|
118
|
+
function wrap(text, prefix = '', width = 80) {
|
|
119
|
+
const continuation = ' '.repeat(prefix.length)
|
|
120
|
+
return text.split('\n').flatMap((paragraph, index) => {
|
|
121
|
+
const lines = []
|
|
122
|
+
let line = index === 0 ? prefix : continuation
|
|
123
|
+
for (const word of paragraph.split(/\s+/)) {
|
|
124
|
+
if (line.trim() && line.length + 1 + word.length > width) {
|
|
125
|
+
lines.push(line)
|
|
126
|
+
line = continuation
|
|
127
|
+
}
|
|
128
|
+
line += (line.trim() && line !== prefix ? ' ' : '') + word
|
|
129
|
+
}
|
|
130
|
+
lines.push(line)
|
|
131
|
+
return lines
|
|
132
|
+
}).join('\n')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function deploymentHelp() {
|
|
136
|
+
const sections = Object.values(DEPLOYMENTS).map(({ label, description, details }, index) => [
|
|
137
|
+
`${index + 1}. ${label}`,
|
|
138
|
+
wrap(description, ' '),
|
|
139
|
+
...Object.entries(details).map(([name, value]) => wrap(value, ` ${name}: `)),
|
|
140
|
+
].join('\n'))
|
|
141
|
+
return [
|
|
142
|
+
'Choosing a deployment target',
|
|
143
|
+
'Keep sensitive CV source private. Publish only .output/public.',
|
|
144
|
+
...sections,
|
|
145
|
+
'Setup steps are shown after selection.',
|
|
146
|
+
].join('\n\n')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function deploymentInstructions(key, packageManager = 'pnpm') {
|
|
150
|
+
const { label, setup, notes = [] } = DEPLOYMENTS[key]
|
|
151
|
+
const commandText = text => packageManager === 'npm'
|
|
152
|
+
? text.replace(/pnpm-lock\.yaml/g, 'package-lock.json').replace(/pnpm check/g, 'npm run check')
|
|
153
|
+
: text
|
|
154
|
+
return [
|
|
155
|
+
`Deployment: ${label}`,
|
|
156
|
+
...notes.map(note => wrap(note, ' ')),
|
|
157
|
+
...setup.map((step, index) => wrap(commandText(step), ` ${index + 1}. `)),
|
|
158
|
+
].join('\n')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Keep help and invalid input in the selection loop, without losing answers. */
|
|
162
|
+
export async function chooseDeployment(ask, log = console.log) {
|
|
163
|
+
const keys = Object.keys(DEPLOYMENTS)
|
|
164
|
+
log('\nDeployment target (you can change this later):')
|
|
165
|
+
keys.forEach((key, index) => log(` ${index + 1}. ${DEPLOYMENTS[key].label}`))
|
|
166
|
+
log(' ?. Help choosing — URLs, requirements and setup')
|
|
167
|
+
|
|
168
|
+
while (true) {
|
|
169
|
+
const choice = (await ask('Choose a number, or ? for help:', '1')).trim()
|
|
170
|
+
if (/^(\?|h|help)$/i.test(choice)) {
|
|
171
|
+
log(`\n${deploymentHelp()}\n`)
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
const key = /^[1-9]\d*$/.test(choice) ? keys[Number(choice) - 1] : undefined
|
|
175
|
+
if (key) return key
|
|
176
|
+
log(`Please choose a number from 1 to ${keys.length}, or ? for help.`)
|
|
177
|
+
}
|
|
178
|
+
}
|
package/index.mjs
CHANGED
|
@@ -1,83 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* pnpm create @anja.codes/vitakit [directory]
|
|
4
|
+
* npm create @anja.codes/vitakit [directory]
|
|
4
5
|
*
|
|
5
|
-
* Copies the template, substitutes the few things that are personal,
|
|
6
|
-
*
|
|
6
|
+
* Copies the template, substitutes the few things that are personal, picks a
|
|
7
|
+
* deployment workflow, and writes the project for the package manager that ran
|
|
8
|
+
* it. Deliberately the *only* scaffolding step vitakit has:
|
|
7
9
|
* everything afterwards is editing files in a repository you own.
|
|
8
10
|
*/
|
|
9
|
-
import {
|
|
11
|
+
import { readdir } from 'node:fs/promises'
|
|
10
12
|
import { existsSync } from 'node:fs'
|
|
11
|
-
import { basename,
|
|
12
|
-
import { fileURLToPath } from 'node:url'
|
|
13
|
+
import { basename, resolve } from 'node:path'
|
|
13
14
|
import { createInterface } from 'node:readline/promises'
|
|
14
15
|
import process from 'node:process'
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const DEPLOYMENTS = {
|
|
19
|
-
'github-pages': { label: 'GitHub Pages (public repository)', keep: ['deploy.yml', 'cv.yml'], host: '_github' },
|
|
20
|
-
'cloudflare': { label: 'Cloudflare Pages (works with a private repository)', keep: ['cloudflare.yml', 'cv.yml'], host: '_github' },
|
|
21
|
-
'rsync': { label: 'Web space via rsync (Forgejo)', keep: ['deploy.yml'], host: '_forgejo' },
|
|
22
|
-
'none': { label: 'None for now', keep: [], host: undefined },
|
|
23
|
-
}
|
|
16
|
+
import { detectPackageManager, scaffold } from './scaffold.mjs'
|
|
17
|
+
import { chooseDeployment, deploymentHelp, deploymentInstructions } from './deployments.mjs'
|
|
24
18
|
|
|
25
19
|
function slugify(value) {
|
|
26
20
|
return value.normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
|
|
27
21
|
.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'my-site'
|
|
28
22
|
}
|
|
29
23
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
.
|
|
35
|
-
.
|
|
36
|
-
|
|
37
|
-
.replaceAll('__SLUG__', answers.slug)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const TEXT_FILE = /\.(md|ya?ml|json|ts|vue|css|bib|tex|svg|txt|yml|template)$/
|
|
41
|
-
|
|
42
|
-
async function scaffold(target, answers) {
|
|
43
|
-
await mkdir(target, { recursive: true })
|
|
44
|
-
await cp(TEMPLATE, target, { recursive: true })
|
|
45
|
-
|
|
46
|
-
// Files that cannot be shipped under their final name: `.gitignore` is
|
|
47
|
-
// excluded by npm, and `package.json` would make the template a workspace.
|
|
48
|
-
for (const [from, to] of [['.gitignore.template', '.gitignore'], ['package.json.template', 'package.json']]) {
|
|
49
|
-
await rename(join(target, from), join(target, to))
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const deployment = DEPLOYMENTS[answers.deployment]
|
|
53
|
-
for (const [dir, name] of [['_github', '.github'], ['_forgejo', '.forgejo']]) {
|
|
54
|
-
const path = join(target, dir)
|
|
55
|
-
if (deployment.host !== dir) {
|
|
56
|
-
await rm(path, { recursive: true, force: true })
|
|
57
|
-
continue
|
|
58
|
-
}
|
|
59
|
-
const workflows = join(path, 'workflows')
|
|
60
|
-
for (const file of await readdir(workflows)) {
|
|
61
|
-
if (!deployment.keep.includes(file)) await rm(join(workflows, file))
|
|
62
|
-
}
|
|
63
|
-
await rename(path, join(target, name))
|
|
24
|
+
async function main() {
|
|
25
|
+
const args = process.argv.slice(2)
|
|
26
|
+
const packageManager = detectPackageManager(process.env.npm_config_user_agent)
|
|
27
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
28
|
+
console.log(`Usage: ${packageManager} create @anja.codes/vitakit [directory]\n\n --help, -h Show this help without creating a project\n --deployment-help Explain deployment targets without creating a project\n`)
|
|
29
|
+
console.log(deploymentHelp())
|
|
30
|
+
return
|
|
64
31
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
for (const item of await readdir(dir, { withFileTypes: true })) {
|
|
69
|
-
const path = join(dir, item.name)
|
|
70
|
-
if (item.isDirectory()) await walk(path)
|
|
71
|
-
else if (TEXT_FILE.test(item.name) || item.name === 'package.json' || item.name === '.gitignore') {
|
|
72
|
-
await writeFile(path, fill(await readFile(path, 'utf8'), answers))
|
|
73
|
-
}
|
|
74
|
-
}
|
|
32
|
+
if (args.includes('--deployment-help')) {
|
|
33
|
+
console.log(deploymentHelp())
|
|
34
|
+
return
|
|
75
35
|
}
|
|
76
|
-
await walk(target)
|
|
77
|
-
}
|
|
78
36
|
|
|
79
|
-
|
|
80
|
-
const [directoryArgument] = process.argv.slice(2).filter(argument => !argument.startsWith('-'))
|
|
37
|
+
const [directoryArgument] = args.filter(argument => !argument.startsWith('-'))
|
|
81
38
|
const interactive = process.stdin.isTTY && !process.env.VITAKIT_CREATE_DEFAULTS
|
|
82
39
|
|
|
83
40
|
const answers = {
|
|
@@ -86,6 +43,7 @@ async function main() {
|
|
|
86
43
|
affiliation: 'Department of Mathematics, University of Graz',
|
|
87
44
|
email: 'you@example.org',
|
|
88
45
|
deployment: 'github-pages',
|
|
46
|
+
packageManager,
|
|
89
47
|
}
|
|
90
48
|
|
|
91
49
|
let directory = directoryArgument
|
|
@@ -104,13 +62,11 @@ async function main() {
|
|
|
104
62
|
answers.email = await ask('Email:', answers.email)
|
|
105
63
|
directory ??= await ask('Directory:', slugify(answers.name))
|
|
106
64
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
rl.close()
|
|
65
|
+
try {
|
|
66
|
+
answers.deployment = await chooseDeployment(ask)
|
|
67
|
+
} finally {
|
|
68
|
+
rl.close()
|
|
69
|
+
}
|
|
114
70
|
}
|
|
115
71
|
|
|
116
72
|
directory ??= slugify(answers.name)
|
|
@@ -124,13 +80,17 @@ async function main() {
|
|
|
124
80
|
await scaffold(target, answers)
|
|
125
81
|
|
|
126
82
|
console.log(`\nCreated ${directory}\n`)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
console.log(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
83
|
+
const pm = answers.packageManager
|
|
84
|
+
const run = pm === 'npm' ? 'npm run' : 'pnpm'
|
|
85
|
+
const step = (command, comment) => console.log(` ${command.padEnd(20)}${comment ? `# ${comment}` : ''}`.trimEnd())
|
|
86
|
+
step('cd ' + directory)
|
|
87
|
+
step(`${pm} install`)
|
|
88
|
+
step(`${run} dev`, 'http://localhost:3000')
|
|
89
|
+
step(`${run} pdf`, 'cv-tex/cv.pdf (needs LaTeX)')
|
|
90
|
+
step(`${run} check`, 'what CI runs')
|
|
91
|
+
console.log()
|
|
92
|
+
console.log('Edit content/ to make it yours. Keep sensitive CV source private.\n')
|
|
93
|
+
console.log(`${deploymentInstructions(answers.deployment, pm)}\n`)
|
|
134
94
|
}
|
|
135
95
|
|
|
136
96
|
main().catch((error) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anja.codes/create-vitakit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Scaffold a vitakit project: an academic website and a LaTeX CV from one set of files.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"nuxt",
|
|
13
13
|
"latex"
|
|
14
14
|
],
|
|
15
|
-
"homepage": "https://
|
|
15
|
+
"homepage": "https://vitakit.anja.codes/guide/tutorial/",
|
|
16
16
|
"bugs": {
|
|
17
17
|
"url": "https://github.com/anja-codes/vitakit/issues"
|
|
18
18
|
},
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"index.mjs",
|
|
30
|
+
"scaffold.mjs",
|
|
31
|
+
"deployments.mjs",
|
|
30
32
|
"template",
|
|
31
33
|
"README.md",
|
|
32
34
|
"LICENSE"
|
package/scaffold.mjs
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { cp, mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { basename, join } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { DEPLOYMENTS } from './deployments.mjs'
|
|
5
|
+
|
|
6
|
+
export { DEPLOYMENTS } from './deployments.mjs'
|
|
7
|
+
|
|
8
|
+
const TEMPLATE = fileURLToPath(new URL('template', import.meta.url))
|
|
9
|
+
|
|
10
|
+
/** Package managers a project can be written for; the first is the default. */
|
|
11
|
+
export const PACKAGE_MANAGERS = ['pnpm', 'npm']
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The package manager that started `create`, from the user agent every package
|
|
15
|
+
* manager sets for the processes it runs (`npm/11.6.0 node/v22.18.0 …`).
|
|
16
|
+
* Anything but npm (pnpm, another one, or running this file directly) gets
|
|
17
|
+
* the default.
|
|
18
|
+
*/
|
|
19
|
+
export function detectPackageManager(userAgent = '') {
|
|
20
|
+
return userAgent.startsWith('npm/') ? 'npm' : 'pnpm'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The template is written for pnpm. For npm, its commands are translated in
|
|
25
|
+
* every text file (scripts, workflows, comments, the pages' own instructions),
|
|
26
|
+
* so a project never mentions a package manager its owner does not use.
|
|
27
|
+
*/
|
|
28
|
+
function forNpm(text, scripts) {
|
|
29
|
+
return text
|
|
30
|
+
// The pnpm setup step, together with the comment lines directly above it.
|
|
31
|
+
.replace(/^(?:[ \t]*#.*\n)*[ \t]*- uses: \S*pnpm\/action-setup@\S+\n/gm, '')
|
|
32
|
+
.replace(/cache: pnpm\b/g, 'cache: npm')
|
|
33
|
+
.replace(/^[ \t]*- corepack (?:enable|install)\n/gm, '')
|
|
34
|
+
.replace(/pnpm install --frozen-lockfile/g, 'npm ci')
|
|
35
|
+
.replace(/pnpm vitakit\b/g, 'npx vitakit')
|
|
36
|
+
// `pnpm pdf --tex-only` -> `npm run pdf -- --tex-only`
|
|
37
|
+
.replace(/pnpm ([\w:-]+)((?: --?[\w-]+)*)/g, (command, script, args) =>
|
|
38
|
+
scripts.includes(script) ? `npm run ${script}${args ? ` --${args}` : ''}` : command)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** YAML placeholders represent whole scalars; prose/TeX placeholders stay raw. */
|
|
42
|
+
function fill(text, answers) {
|
|
43
|
+
const values = {
|
|
44
|
+
NAME: answers.name,
|
|
45
|
+
TITLE: answers.title,
|
|
46
|
+
AFFILIATION: answers.affiliation,
|
|
47
|
+
EMAIL: answers.email,
|
|
48
|
+
SLUG: answers.slug,
|
|
49
|
+
SUMMARY: `${answers.title} at ${answers.affiliation}, working on your research area.`,
|
|
50
|
+
}
|
|
51
|
+
// One callback pass preserves literal $ replacements and placeholder-like
|
|
52
|
+
// text in answers. JSON strings are valid quoted YAML scalars, including
|
|
53
|
+
// embedded quotes, backslashes and newlines.
|
|
54
|
+
return text.replace(/__(YAML_)?(NAME|TITLE|AFFILIATION|EMAIL|SLUG|SUMMARY)__/g,
|
|
55
|
+
(_, yaml, key) => yaml ? JSON.stringify(values[key]) : values[key])
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const TEXT_FILE = /\.(md|ya?ml|json|ts|vue|css|bib|tex|svg|txt|yml|template)$/
|
|
59
|
+
const TEXT_NAMES = new Set(['package.json', '.gitignore', 'latexmkrc'])
|
|
60
|
+
|
|
61
|
+
/** Internal creator implementation, separate from the interactive CLI. */
|
|
62
|
+
export async function scaffold(target, answers) {
|
|
63
|
+
const deployment = DEPLOYMENTS[answers.deployment]
|
|
64
|
+
if (!deployment || !Object.hasOwn(DEPLOYMENTS, answers.deployment)) {
|
|
65
|
+
throw new Error(`Unknown deployment target: ${answers.deployment}`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await mkdir(target, { recursive: true })
|
|
69
|
+
await cp(TEMPLATE, target, {
|
|
70
|
+
recursive: true,
|
|
71
|
+
// Finder metadata can exist in a checkout but must not reach a project.
|
|
72
|
+
filter: source => basename(source) !== '.DS_Store',
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// Files that cannot be shipped under their final name: `.gitignore` is
|
|
76
|
+
// excluded by npm, and `package.json` would make the template a workspace.
|
|
77
|
+
for (const [from, to] of [['.gitignore.template', '.gitignore'], ['package.json.template', 'package.json']]) {
|
|
78
|
+
await rename(join(target, from), join(target, to))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const [dir, name] of [['_github', '.github'], ['_forgejo', '.forgejo']]) {
|
|
82
|
+
const path = join(target, dir)
|
|
83
|
+
const keep = deployment.files.filter(file => file.startsWith(`${name}/workflows/`))
|
|
84
|
+
if (!keep.length) {
|
|
85
|
+
await rm(path, { recursive: true, force: true })
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
const workflows = join(path, 'workflows')
|
|
89
|
+
for (const file of await readdir(workflows)) {
|
|
90
|
+
if (!keep.includes(`${name}/workflows/${file}`)) await rm(join(workflows, file))
|
|
91
|
+
}
|
|
92
|
+
await rename(path, join(target, name))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Standalone provider configuration is just as target-specific as workflows.
|
|
96
|
+
for (const [from, to] of [['gitlab-ci.yml.template', '.gitlab-ci.yml'], ['wrangler.json.template', 'wrangler.json']]) {
|
|
97
|
+
if (deployment.files.includes(to)) await rename(join(target, from), join(target, to))
|
|
98
|
+
else await rm(join(target, from))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const manifest = join(target, 'package.json')
|
|
102
|
+
const npm = answers.packageManager === 'npm'
|
|
103
|
+
const scripts = Object.keys(JSON.parse(await readFile(manifest, 'utf8')).scripts)
|
|
104
|
+
|
|
105
|
+
// Substitute placeholders in every text file, and translate commands for npm.
|
|
106
|
+
const walk = async (dir) => {
|
|
107
|
+
for (const item of await readdir(dir, { withFileTypes: true })) {
|
|
108
|
+
const path = join(dir, item.name)
|
|
109
|
+
if (item.isDirectory()) await walk(path)
|
|
110
|
+
else if (TEXT_FILE.test(item.name) || TEXT_NAMES.has(item.name)) {
|
|
111
|
+
const text = fill(await readFile(path, 'utf8'), answers)
|
|
112
|
+
await writeFile(path, npm ? forNpm(text, scripts) : text)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
await walk(target)
|
|
117
|
+
|
|
118
|
+
// `packageManager` pins pnpm for Corepack and for pnpm/action-setup.
|
|
119
|
+
if (npm) {
|
|
120
|
+
const pkg = JSON.parse(await readFile(manifest, 'utf8'))
|
|
121
|
+
delete pkg.packageManager
|
|
122
|
+
await writeFile(manifest, `${JSON.stringify(pkg, null, 2)}\n`)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Builds the site and uploads only the checked output to Codeberg Pages.
|
|
2
|
+
# Codeberg Pages now uses git-pages; no pages branch or webhook is needed here.
|
|
3
|
+
# Enable Actions in repository settings. Codeberg's hosted runners require a
|
|
4
|
+
# PUBLIC, freely licensed repository. For private CV source, use your own
|
|
5
|
+
# compatible runner and change runs-on below; do not make sensitive source public.
|
|
6
|
+
# The automatic token authorizes the matching <owner>.codeberg.page/<repo>/ site.
|
|
7
|
+
# For a root site or custom domain, change BOTH NUXT_APP_BASE_URL and site below.
|
|
8
|
+
# Custom domains also need DNS authorization: https://docs.codeberg.org/codeberg-pages/using-custom-domain/
|
|
9
|
+
name: Deploy (Codeberg Pages)
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
push:
|
|
13
|
+
branches: [main]
|
|
14
|
+
workflow_dispatch:
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
deploy:
|
|
18
|
+
if: ${{ forgejo.ref == 'refs/heads/main' }}
|
|
19
|
+
# Use a Docker-capable runner; Nuxt builds need more than the tiny tier.
|
|
20
|
+
runs-on: codeberg-medium
|
|
21
|
+
env:
|
|
22
|
+
NUXT_APP_BASE_URL: /${{ forgejo.event.repository.name }}/
|
|
23
|
+
steps:
|
|
24
|
+
- uses: https://github.com/actions/checkout@v7
|
|
25
|
+
- uses: https://github.com/pnpm/action-setup@v6
|
|
26
|
+
- uses: https://github.com/actions/setup-node@v7
|
|
27
|
+
with:
|
|
28
|
+
node-version: 22
|
|
29
|
+
cache: pnpm
|
|
30
|
+
- run: pnpm install --frozen-lockfile
|
|
31
|
+
- run: pnpm check
|
|
32
|
+
- uses: https://codeberg.org/git-pages/action@v2
|
|
33
|
+
with:
|
|
34
|
+
site: https://${{ forgejo.repository_owner }}.codeberg.page/${{ forgejo.event.repository.name }}/
|
|
35
|
+
server: codeberg.page
|
|
36
|
+
token: ${{ forgejo.token }}
|
|
37
|
+
source: .output/public/
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# Builds the site and deploys it to Cloudflare Pages.
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
3
|
+
# GitHub Actions builds the site and uploads only .output/public; the source
|
|
4
|
+
# repository can stay private. The site is public at <project>.pages.dev (or
|
|
5
|
+
# your custom domain). Free-tier hosting and GitHub Actions usage limits apply.
|
|
5
6
|
#
|
|
6
|
-
#
|
|
7
|
-
# Set
|
|
7
|
+
# Setup: create a Pages Direct Upload project with production branch `main`.
|
|
8
|
+
# Set --project-name below to its name.
|
|
9
|
+
# Secrets: CLOUDFLARE_API_TOKEN (Pages edit permission), CLOUDFLARE_ACCOUNT_ID.
|
|
8
10
|
name: Deploy (Cloudflare Pages)
|
|
9
11
|
|
|
10
12
|
on:
|
|
@@ -24,7 +24,7 @@ jobs:
|
|
|
24
24
|
- run: pnpm install --frozen-lockfile
|
|
25
25
|
# Generate the .tex files here: the LaTeX action runs in its own container,
|
|
26
26
|
# without Node. cv-tex/latexmkrc lets it find the generated document class.
|
|
27
|
-
- run: pnpm
|
|
27
|
+
- run: pnpm pdf --tex-only
|
|
28
28
|
- uses: xu-cheng/latex-action@v4
|
|
29
29
|
with:
|
|
30
30
|
working_directory: cv-tex
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Builds the site and deploys static assets to Cloudflare Workers (not Pages).
|
|
2
|
+
# wrangler.json points exclusively to .output/public. No Worker script or SSR.
|
|
3
|
+
# Setup: enable your account's workers.dev subdomain and choose an available
|
|
4
|
+
# Worker name in wrangler.json, or configure a custom domain there.
|
|
5
|
+
# Secrets: CLOUDFLARE_API_TOKEN (Workers Scripts edit permission),
|
|
6
|
+
# CLOUDFLARE_ACCOUNT_ID. The source repository can stay private.
|
|
7
|
+
name: Deploy (Cloudflare Workers)
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
push:
|
|
11
|
+
branches: [main]
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
concurrency:
|
|
18
|
+
group: cloudflare-workers
|
|
19
|
+
cancel-in-progress: true
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
deploy:
|
|
23
|
+
if: ${{ github.ref == 'refs/heads/main' }}
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v7
|
|
27
|
+
- uses: pnpm/action-setup@v6
|
|
28
|
+
- uses: actions/setup-node@v7
|
|
29
|
+
with:
|
|
30
|
+
node-version: 22
|
|
31
|
+
cache: pnpm
|
|
32
|
+
- run: pnpm install --frozen-lockfile
|
|
33
|
+
- run: pnpm check
|
|
34
|
+
- uses: cloudflare/wrangler-action@v4
|
|
35
|
+
with:
|
|
36
|
+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
37
|
+
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
38
|
+
wranglerVersion: '4'
|
|
39
|
+
command: deploy
|
|
@@ -34,6 +34,18 @@ a {
|
|
|
34
34
|
.vita-section {
|
|
35
35
|
@apply divide-y divide-neutral-100;
|
|
36
36
|
}
|
|
37
|
+
.vita-section-group {
|
|
38
|
+
@apply mt-10;
|
|
39
|
+
}
|
|
40
|
+
.vita-section-group__title {
|
|
41
|
+
@apply text-xl font-semibold;
|
|
42
|
+
}
|
|
43
|
+
.vita-section-group > .vita-section {
|
|
44
|
+
@apply mt-3;
|
|
45
|
+
}
|
|
46
|
+
.vita-section-group__subtitle {
|
|
47
|
+
@apply mt-6 text-sm font-semibold text-neutral-600;
|
|
48
|
+
}
|
|
37
49
|
.vita-links {
|
|
38
50
|
@apply flex flex-wrap gap-x-4 gap-y-1;
|
|
39
51
|
}
|
|
@@ -3,18 +3,16 @@
|
|
|
3
3
|
* The CV page: several sections composed in the order you want them.
|
|
4
4
|
*
|
|
5
5
|
* Section order, headings and which sections appear are decided here, not in
|
|
6
|
-
* the data — exactly as `cv.tex` decides them for the PDF.
|
|
6
|
+
* the data — exactly as `cv-tex/cv.tex` decides them for the PDF. One line per
|
|
7
|
+
* section: move a line to move a section, delete it to leave the section out.
|
|
8
|
+
* A section without entries is not shown, heading included.
|
|
7
9
|
*/
|
|
8
10
|
const profile = useProfile()
|
|
9
|
-
const page = usePage('/cv/')
|
|
11
|
+
const page = usePage('/cv/')
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
{
|
|
13
|
-
|
|
14
|
-
{ id: 'teaching', label: 'Teaching' },
|
|
15
|
-
{ id: 'supervision', label: 'Supervised theses' },
|
|
16
|
-
{ id: 'talks', label: 'Talks' },
|
|
17
|
-
]
|
|
13
|
+
if (!page) {
|
|
14
|
+
throw createError({ statusCode: 404, statusMessage: 'No page for /cv/ (expected content/pages/cv.md)', fatal: true })
|
|
15
|
+
}
|
|
18
16
|
|
|
19
17
|
useHead({
|
|
20
18
|
title: `${page.title} – ${profile.name}`,
|
|
@@ -24,16 +22,13 @@ useHead({
|
|
|
24
22
|
|
|
25
23
|
<template>
|
|
26
24
|
<article>
|
|
27
|
-
<h1 class="text-3xl font-semibold"
|
|
28
|
-
{{ page.title }}
|
|
29
|
-
</h1>
|
|
25
|
+
<h1 class="text-3xl font-semibold" v-html="page.titleHtml" />
|
|
30
26
|
<VitaMarkdown class="mt-6" :html="page.bodyHtml" />
|
|
31
27
|
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
</section>
|
|
28
|
+
<VitaSection id="employment" title="Employment" />
|
|
29
|
+
<VitaSection id="education" title="Education" />
|
|
30
|
+
<VitaSection id="teaching" title="Teaching" />
|
|
31
|
+
<VitaSection id="supervision" title="Supervised theses" />
|
|
32
|
+
<VitaSection id="talks" title="Talks" />
|
|
38
33
|
</article>
|
|
39
34
|
</template>
|
|
@@ -30,12 +30,12 @@ useHead({
|
|
|
30
30
|
<h1 class="text-3xl font-semibold">
|
|
31
31
|
{{ profile.name }}
|
|
32
32
|
</h1>
|
|
33
|
-
<p v-if="profile.title" class="text-neutral-600">
|
|
33
|
+
<p v-if="profile.title || profile.affiliation" class="text-neutral-600">
|
|
34
34
|
{{ profile.title }}
|
|
35
|
+
<template v-if="profile.title && profile.affiliation"> · </template>
|
|
35
36
|
<template v-if="profile.affiliation">
|
|
36
|
-
|
|
37
|
-
<
|
|
38
|
-
<template v-else>{{ profile.affiliation.name }}</template>
|
|
37
|
+
<a v-if="profile.affiliation.url" :href="profile.affiliation.url">{{ profile.affiliation.label }}</a>
|
|
38
|
+
<template v-else>{{ profile.affiliation.label }}</template>
|
|
39
39
|
</template>
|
|
40
40
|
</p>
|
|
41
41
|
<p v-if="profile.email" class="mt-1 text-sm">
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
* escape hatch when a page needs prose *and* components.
|
|
7
7
|
*/
|
|
8
8
|
const profile = useProfile()
|
|
9
|
-
const page = usePage('/publications/')
|
|
9
|
+
const page = usePage('/publications/')
|
|
10
|
+
|
|
11
|
+
if (!page) {
|
|
12
|
+
throw createError({ statusCode: 404, statusMessage: 'No page for /publications/ (expected content/pages/publications.md)', fatal: true })
|
|
13
|
+
}
|
|
10
14
|
|
|
11
15
|
useHead({
|
|
12
16
|
title: `${page.title} – ${profile.name}`,
|
|
@@ -16,9 +20,7 @@ useHead({
|
|
|
16
20
|
|
|
17
21
|
<template>
|
|
18
22
|
<article>
|
|
19
|
-
<h1 class="text-3xl font-semibold"
|
|
20
|
-
{{ page.title }}
|
|
21
|
-
</h1>
|
|
23
|
+
<h1 class="text-3xl font-semibold" v-html="page.titleHtml" />
|
|
22
24
|
<VitaMarkdown class="mt-6" :html="page.bodyHtml" />
|
|
23
25
|
<VitaPublications class="mt-8" />
|
|
24
26
|
</article>
|
|
@@ -4,6 +4,6 @@ description: The same data that produces the PDF.
|
|
|
4
4
|
order: 30
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
Build the PDF version with `pnpm
|
|
7
|
+
Build the PDF version with `pnpm pdf`. The sections below come from the
|
|
8
8
|
YAML files in `content/`; their order is decided in `app/pages/cv.vue`, and the
|
|
9
9
|
order in the PDF in `cv-tex/cv.tex`.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
title:
|
|
2
|
+
title: __YAML_NAME__
|
|
3
3
|
order: 0
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -8,4 +8,4 @@ Replace this page with a few sentences about yourself: it lives in
|
|
|
8
8
|
|
|
9
9
|
Everything on this site comes from the files in `content/`, and the same data
|
|
10
10
|
produces `cv-tex/cv.pdf`. Add a page by adding a Markdown file; add a CV
|
|
11
|
-
section by adding a YAML file — `pnpm vitakit add outreach` sets one up.
|
|
11
|
+
section by adding a YAML file — `pnpm vitakit add section outreach` sets one up.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# You, and the shape of your site. The only file that is not a CV section.
|
|
2
|
-
name:
|
|
3
|
-
title:
|
|
2
|
+
name: __YAML_NAME__
|
|
3
|
+
title: __YAML_TITLE__
|
|
4
4
|
affiliation:
|
|
5
|
-
name:
|
|
6
|
-
email:
|
|
5
|
+
name: __YAML_AFFILIATION__
|
|
6
|
+
email: __YAML_EMAIL__
|
|
7
7
|
|
|
8
8
|
# Where the site is published. Only the PDF needs it, to link files on the
|
|
9
9
|
# site such as /slides/talk.pdf; delete the # once you know the address.
|
|
@@ -19,8 +19,7 @@ links:
|
|
|
19
19
|
|
|
20
20
|
# One or two sentences for search engines and link previews. What visitors
|
|
21
21
|
# read is content/pages/index.md, not this.
|
|
22
|
-
summary:
|
|
23
|
-
__TITLE__ at __AFFILIATION__, working on your research area.
|
|
22
|
+
summary: __YAML_SUMMARY__
|
|
24
23
|
|
|
25
24
|
# Navigation, in the order it should appear. Adding a page does not change
|
|
26
25
|
# this list: the wording and the order are yours.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
% The non-standard `status` field decides the grouping on /publications/ and in
|
|
5
5
|
% the PDF: published, accepted, preprint, software or other; without it, an
|
|
6
6
|
% entry is published. To change the groups:
|
|
7
|
-
% pnpm vitakit
|
|
7
|
+
% pnpm vitakit customize publications # -> schemas/publications.ts
|
|
8
8
|
%
|
|
9
9
|
% `doi`, `arxiv`, `mrnumber` and `zbl` become links; `eprint` with
|
|
10
10
|
% `eprinttype = {arxiv}` (or `archivePrefix`) works as `arxiv` does, so exports
|
package/template/cv-tex/cv.tex
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
% Your CV. This file decides the ORDER of things; cv.yml
|
|
1
|
+
% Your CV. This file decides the ORDER of things and their headings; cv.yml
|
|
2
|
+
% decides what goes in. app/pages/cv.vue does the same for the website.
|
|
2
3
|
%
|
|
3
4
|
% Styling: redefine the macros in preamble.tex. For deeper changes run
|
|
4
5
|
% pnpm vitakit eject vitakit-cv.cls
|
|
@@ -15,7 +16,7 @@
|
|
|
15
16
|
\addbibresource{../content/publications.bib}
|
|
16
17
|
|
|
17
18
|
\input{preamble.tex}
|
|
18
|
-
\
|
|
19
|
+
\vitainput{profile}
|
|
19
20
|
|
|
20
21
|
\begin{document}
|
|
21
22
|
\vitaheader
|
|
@@ -25,14 +26,14 @@
|
|
|
25
26
|
% sections vitakit generates from content/.
|
|
26
27
|
% \input{prose/research-statement.tex}
|
|
27
28
|
|
|
28
|
-
\vitasection{employment}
|
|
29
|
-
\vitasection{education}
|
|
30
|
-
\vitasection{teaching}
|
|
29
|
+
\vitasection[Employment]{employment}
|
|
30
|
+
\vitasection[Education]{education}
|
|
31
|
+
\vitasection[Teaching]{teaching}
|
|
31
32
|
|
|
32
33
|
\section{Publications}
|
|
33
34
|
\vitainput{publications}
|
|
34
35
|
|
|
35
|
-
\vitasection{supervision}
|
|
36
|
-
\vitasection{talks}
|
|
36
|
+
\vitasection[Supervised theses]{supervision}
|
|
37
|
+
\vitasection[Selected talks]{talks}
|
|
37
38
|
|
|
38
39
|
\end{document}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Lets a plain `latexmk cv.tex` (in CI, or from an editor) find the document
|
|
2
|
-
# class, which `pnpm
|
|
2
|
+
# class, which `pnpm pdf` writes to generated/<document>/. A copy in this
|
|
3
3
|
# directory (`pnpm vitakit eject vitakit-cv.cls`) still wins: `.` comes first.
|
|
4
4
|
# The trailing separator keeps TeX's default search path.
|
|
5
5
|
$ENV{'TEXINPUTS'} = join($search_path_separator, '.', './generated//', $ENV{'TEXINPUTS'} // '');
|
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
% vitakit-cv.cls, so redefining them changes the design without touching
|
|
3
3
|
% anything vitakit produces.
|
|
4
4
|
|
|
5
|
-
%
|
|
6
|
-
|
|
7
|
-
\vitasectiontitle{
|
|
8
|
-
\vitasectiontitle{teaching}{Teaching}
|
|
9
|
-
\vitasectiontitle{talks}{Selected talks}
|
|
10
|
-
\vitasectiontitle{supervision}{Supervised theses}
|
|
5
|
+
% Headings are written where a section is included, \vitasection[Talks]{talks}.
|
|
6
|
+
% For one wording shared by every document that includes this preamble:
|
|
7
|
+
% \vitasectiontitle{talks}{Selected talks}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Builds and checks the site, then publishes only the static output.
|
|
2
|
+
# Requires GitLab 17.10+ with Pages and a Docker-compatible CI runner enabled.
|
|
3
|
+
# Private source projects are supported; set Pages access to Everyone for a
|
|
4
|
+
# public website (Settings -> General -> Visibility, project features, permissions).
|
|
5
|
+
# For a root-level custom domain, set NUXT_APP_BASE_URL=/ in CI/CD variables.
|
|
6
|
+
image: node:22-bookworm
|
|
7
|
+
|
|
8
|
+
stages: [deploy]
|
|
9
|
+
|
|
10
|
+
deploy-pages:
|
|
11
|
+
stage: deploy
|
|
12
|
+
before_script:
|
|
13
|
+
- corepack enable
|
|
14
|
+
- corepack install
|
|
15
|
+
- pnpm install --frozen-lockfile
|
|
16
|
+
script:
|
|
17
|
+
# CI_PAGES_URL handles unique domains, project paths and subgroup paths.
|
|
18
|
+
# An explicit CI/CD variable can override it for a custom domain.
|
|
19
|
+
- |
|
|
20
|
+
if [ -z "${NUXT_APP_BASE_URL:-}" ]; then
|
|
21
|
+
NUXT_APP_BASE_URL="$(node -e 'const path = new URL(process.env.CI_PAGES_URL).pathname; process.stdout.write(path.endsWith("/") ? path : path + "/")')"
|
|
22
|
+
export NUXT_APP_BASE_URL
|
|
23
|
+
fi
|
|
24
|
+
- pnpm check
|
|
25
|
+
pages:
|
|
26
|
+
publish: .output/public
|
|
27
|
+
rules:
|
|
28
|
+
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
@@ -12,12 +12,11 @@
|
|
|
12
12
|
"preview": "nuxt preview",
|
|
13
13
|
"postinstall": "nuxt prepare",
|
|
14
14
|
"typecheck": "nuxt typecheck",
|
|
15
|
-
"check
|
|
16
|
-
"
|
|
17
|
-
"build:cv": "vitakit-build-cv"
|
|
15
|
+
"check": "pnpm typecheck && pnpm generate && vitakit verify",
|
|
16
|
+
"pdf": "vitakit pdf"
|
|
18
17
|
},
|
|
19
18
|
"dependencies": {
|
|
20
|
-
"@anja.codes/vitakit": "^0.
|
|
19
|
+
"@anja.codes/vitakit": "^0.2.0",
|
|
21
20
|
"@nuxt/fonts": "^0.14.0",
|
|
22
21
|
"@tailwindcss/vite": "^4.3.3",
|
|
23
22
|
"nuxt": "^4.5.2",
|