create-bike-blog 0.1.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/index.js ADDED
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { execSync } from 'node:child_process';
6
+ import readline from 'node:readline';
7
+
8
+ // --- Template engine ---
9
+
10
+ export function renderTemplate(content, vars) {
11
+ return content.replace(/\{\{(\w+)\}\}/g, (match, key) => {
12
+ return key in vars ? vars[key] : match;
13
+ });
14
+ }
15
+
16
+ // --- File operations ---
17
+
18
+ function copyTemplate(templateDir, destDir, vars) {
19
+ for (const entry of fs.readdirSync(templateDir, { withFileTypes: true })) {
20
+ const srcPath = path.join(templateDir, entry.name);
21
+ const destName = entry.name.replace(/\.tpl$/, '');
22
+ const destPath = path.join(destDir, destName);
23
+
24
+ if (entry.isDirectory()) {
25
+ fs.mkdirSync(destPath, { recursive: true });
26
+ copyTemplate(srcPath, destPath, vars);
27
+ } else {
28
+ let content = fs.readFileSync(srcPath, 'utf-8');
29
+ if (entry.name.endsWith('.tpl')) {
30
+ content = renderTemplate(content, vars);
31
+ }
32
+ fs.writeFileSync(destPath, content);
33
+ }
34
+ }
35
+ }
36
+
37
+ // --- Prompts ---
38
+
39
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
40
+ const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
41
+
42
+ // --- Main ---
43
+
44
+ async function main() {
45
+ const args = process.argv.slice(2);
46
+
47
+ if (args.length < 2 || args.includes('--help') || args.includes('-h')) {
48
+ console.log(`
49
+ Usage: npx create-bike-blog <folder> <domain>
50
+
51
+ Example: npx create-bike-blog bike-blog eljojo.bike
52
+ `);
53
+ process.exit(args.includes('--help') || args.includes('-h') ? 0 : 1);
54
+ }
55
+
56
+ const folder = args[0];
57
+ const domain = args[1];
58
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
59
+ const destDir = path.resolve(folder);
60
+
61
+ if (fs.existsSync(destDir)) {
62
+ console.error(`\n Error: ${folder}/ already exists.\n`);
63
+ process.exit(1);
64
+ }
65
+
66
+ console.log(`
67
+ Welcome to whereto.bike!
68
+
69
+ Here's how setting up your blog works:
70
+
71
+ 1. Bootstrap (you are here)
72
+ Creating your project in ${folder}/
73
+
74
+ 2. Edit
75
+ Customize your config, write your about page, add some rides
76
+
77
+ 3. npm run setup
78
+ Connects GitHub and Cloudflare so deploys work automatically
79
+
80
+ 4. git push
81
+ Your blog is live at ${domain}!
82
+ `);
83
+
84
+ // Create project directory
85
+ fs.mkdirSync(destDir, { recursive: true });
86
+
87
+ const folderName = path.basename(destDir);
88
+ const vars = {
89
+ FOLDER: folderName,
90
+ DOMAIN: domain,
91
+ TIMEZONE: timezone,
92
+ };
93
+
94
+ // Copy and render templates
95
+ const templateDir = new URL('./templates', import.meta.url).pathname;
96
+ copyTemplate(templateDir, destDir, vars);
97
+
98
+ // Rename dotfiles (npm strips .gitignore from published packages)
99
+ const renames = { gitignore: '.gitignore', gitattributes: '.gitattributes', env: '.env' };
100
+ for (const [from, to] of Object.entries(renames)) {
101
+ const src = path.join(destDir, from);
102
+ const dest = path.join(destDir, to);
103
+ if (fs.existsSync(src)) fs.renameSync(src, dest);
104
+ }
105
+
106
+ // Move github/ to .github/
107
+ const githubSrc = path.join(destDir, 'github');
108
+ const githubDest = path.join(destDir, '.github');
109
+ if (fs.existsSync(githubSrc)) {
110
+ fs.renameSync(githubSrc, githubDest);
111
+ }
112
+
113
+
114
+ console.log(` Files created in ${folder}/\n`);
115
+
116
+ // Install dependencies
117
+ const install = await ask(' Install dependencies? (npm install) [Y/n] ');
118
+ if (install.toLowerCase() !== 'n') {
119
+ try {
120
+ execSync('npm install', { cwd: destDir, stdio: 'inherit' });
121
+ } catch {
122
+ console.error('\n npm install failed. You can retry manually: cd ' + folder + ' && npm install\n');
123
+ }
124
+ } else {
125
+ console.log(`\n Skipped. Run it yourself:\n\n cd ${folder} && npm install\n`);
126
+ }
127
+
128
+ // Initialize git repo
129
+ const gitInit = await ask(' Initialize git repo? (git init + first commit) [Y/n] ');
130
+ if (gitInit.toLowerCase() !== 'n') {
131
+ execSync('git init', { cwd: destDir, stdio: 'pipe' });
132
+ execSync('git add -A', { cwd: destDir, stdio: 'pipe' });
133
+ execSync('git commit -m "initial blog scaffold"', { cwd: destDir, stdio: 'pipe' });
134
+ console.log('\n ✓ Git repo initialized with first commit.\n');
135
+ } else {
136
+ console.log(`\n Skipped. Run it yourself:\n\n cd ${folder} && git init && git add -A && git commit -m "initial blog scaffold"\n`);
137
+ }
138
+
139
+ console.log(` Next steps:
140
+
141
+ cd ${folder}
142
+ # edit blog/config.yml and blog/pages/about.md
143
+ npm run setup
144
+ `);
145
+
146
+ rl.close();
147
+ }
148
+
149
+ // Only run when invoked directly (not when imported for tests)
150
+ const isDirectRun = process.argv[1] &&
151
+ (process.argv[1].endsWith('/index.js') || process.argv[1].endsWith('/create-bike-blog'));
152
+ if (isDirectRun) {
153
+ main();
154
+ }
package/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "create-bike-blog",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a new whereto.bike cycling blog",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-bike-blog": "./index.js"
8
+ },
9
+ "files": ["index.js", "sync.js", "templates/"],
10
+ "license": "AGPL-3.0"
11
+ }
package/sync.js ADDED
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Syncs template files from the whereto-bike package into your blog repo.
5
+ * Run from the blog repo: npm run sync
6
+ *
7
+ * Updates:
8
+ * - .github/workflows/ — CI/deploy/update actions (rendered from .tpl templates)
9
+ * - src/middleware.ts — re-export of package middleware
10
+ * - scripts/setup.js — interactive setup helper
11
+ * - astro.config.mjs — Astro config
12
+ * - tsconfig.json — TypeScript config
13
+ */
14
+
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+
18
+ export function renderTemplate(content, vars) {
19
+ return content.replace(/\{\{(\w+)\}\}/g, (match, key) => {
20
+ return key in vars ? vars[key] : match;
21
+ });
22
+ }
23
+
24
+ /** Read a top-level scalar from a simple YAML file. */
25
+ function readYamlField(filePath, field) {
26
+ const content = fs.readFileSync(filePath, 'utf-8');
27
+ const re = new RegExp(`^${field}:\\s*["']?([^"'\\n]+?)["']?\\s*$`, 'm');
28
+ const match = content.match(re);
29
+ return match ? match[1] : '';
30
+ }
31
+
32
+ export function syncFile(srcPath, destPath, vars) {
33
+ let content = fs.readFileSync(srcPath, 'utf-8');
34
+ if (srcPath.endsWith('.tpl')) {
35
+ content = renderTemplate(content, vars);
36
+ }
37
+
38
+ const existing = fs.existsSync(destPath) ? fs.readFileSync(destPath, 'utf-8') : null;
39
+ if (existing !== content) {
40
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
41
+ fs.writeFileSync(destPath, content);
42
+ return true;
43
+ }
44
+ return false;
45
+ }
46
+
47
+ // --- Main execution (only when run directly, not when imported) ---
48
+ const isMain = process.argv[1] && new URL(`file://${process.argv[1]}`).href === import.meta.url;
49
+
50
+ if (isMain) {
51
+ const cwd = process.cwd();
52
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf-8'));
53
+
54
+ const configPath = path.join(cwd, 'blog', 'config.yml');
55
+ if (!fs.existsSync(configPath)) {
56
+ console.error(` Error: blog/config.yml not found. Is this a bike blog repo?`);
57
+ process.exit(1);
58
+ }
59
+
60
+ const vars = {
61
+ FOLDER: pkg.name,
62
+ DOMAIN: readYamlField(configPath, 'domain'),
63
+ TIMEZONE: Intl.DateTimeFormat().resolvedOptions().timeZone,
64
+ };
65
+
66
+ const templateRoot = new URL('./templates', import.meta.url).pathname;
67
+ let updated = 0;
68
+
69
+ // --- Sync CI workflows (.tpl rendered) ---
70
+ const workflowDir = path.join(templateRoot, 'github', 'workflows');
71
+ const outWorkflowDir = path.join(cwd, '.github', 'workflows');
72
+ for (const file of fs.readdirSync(workflowDir)) {
73
+ const outName = file.replace(/\.tpl$/, '');
74
+ if (syncFile(path.join(workflowDir, file), path.join(outWorkflowDir, outName), vars)) {
75
+ console.log(` updated .github/workflows/${outName}`);
76
+ updated++;
77
+ }
78
+ }
79
+
80
+ // --- Sync source files (copied as-is, no templating) ---
81
+ const sourceFiles = [
82
+ 'src/middleware.ts',
83
+ 'src/content.config.ts',
84
+ 'scripts/setup.js',
85
+ 'astro.config.mjs',
86
+ 'tsconfig.json',
87
+ ];
88
+
89
+ for (const rel of sourceFiles) {
90
+ const srcPath = path.join(templateRoot, rel);
91
+ if (!fs.existsSync(srcPath)) continue;
92
+ if (syncFile(srcPath, path.join(cwd, rel), vars)) {
93
+ console.log(` updated ${rel}`);
94
+ updated++;
95
+ }
96
+ }
97
+
98
+ // --- Summary ---
99
+ if (updated === 0) {
100
+ console.log(' Everything is up to date.');
101
+ } else {
102
+ console.log(`\n ${updated} file(s) updated. Review and commit the changes.`);
103
+ }
104
+ }
@@ -0,0 +1,17 @@
1
+ // Auto-generated by `npm run sync` — local changes will be overwritten.
2
+ // Astro configuration wired to the whereto-bike platform.
3
+ import 'dotenv/config';
4
+ import { defineConfig } from 'astro/config';
5
+ import { getAdapter } from 'whereto-bike/lib/adapter';
6
+ import preact from '@astrojs/preact';
7
+ import { wheretoBike, cspConfig } from 'whereto-bike';
8
+
9
+ export default defineConfig({
10
+ site: process.env.SITE_URL,
11
+ adapter: await getAdapter(process.env.RUNTIME),
12
+ security: cspConfig(),
13
+ integrations: [
14
+ preact(),
15
+ ...wheretoBike({ consumerRoot: import.meta.dirname }),
16
+ ],
17
+ });
@@ -0,0 +1,24 @@
1
+ instance_type: blog
2
+ name: "My Bike Blog"
3
+ display_name: {{DOMAIN}}
4
+ tagline: "bike rides and adventures"
5
+ description: "A cycling blog — rides, tours, and adventures."
6
+ domain: {{DOMAIN}}
7
+ cdn_url: https://cdn.{{DOMAIN}}
8
+ videos_cdn_url: https://cdn.{{DOMAIN}}
9
+ timezone: {{TIMEZONE}}
10
+ locale: en
11
+ locales: [en]
12
+ author:
13
+ name: ""
14
+ email: ""
15
+ url: https://{{DOMAIN}}
16
+ center:
17
+ lat: 0
18
+ lng: 0
19
+ bounds:
20
+ north: 90
21
+ south: -90
22
+ east: 180
23
+ west: -180
24
+ place_categories: {}
@@ -0,0 +1,4 @@
1
+ ---
2
+ title: About
3
+ ---
4
+ Welcome to my cycling blog! I ride bikes and write about it.
@@ -0,0 +1,4 @@
1
+ CONTENT_DIR=.
2
+ CITY=blog
3
+ RUNTIME=local
4
+ SITE_URL=https://{{DOMAIN}}
@@ -0,0 +1,8 @@
1
+ {
2
+ description = "{{DOMAIN}} — personal cycling blog";
3
+
4
+ inputs.bike-app.url = "github:eljojo/bike-app-astro";
5
+
6
+ outputs = { bike-app, ... }:
7
+ bike-app.outputs;
8
+ }
@@ -0,0 +1 @@
1
+ *.gpx filter=lfs diff=lfs merge=lfs -text
@@ -0,0 +1,56 @@
1
+ # Auto-generated by `npm run sync` — local changes will be overwritten.
2
+ # Builds the site on pull requests to verify nothing is broken.
3
+ name: CI
4
+
5
+ on:
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: ci-${{ github.head_ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ build:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v6
18
+
19
+ - name: LFS cache period
20
+ id: lfs-period
21
+ run: echo "period=$(( $(date +%s) / 1209600 ))" >> "$GITHUB_OUTPUT"
22
+
23
+ - name: Restore LFS cache
24
+ uses: actions/cache@v5
25
+ with:
26
+ path: .git/lfs
27
+ key: lfs-${{ steps.lfs-period.outputs.period }}
28
+ restore-keys: lfs-
29
+
30
+ - name: Pull LFS files
31
+ run: git lfs pull
32
+
33
+ - uses: actions/setup-node@v6
34
+ with:
35
+ node-version: 22
36
+ cache: 'npm'
37
+
38
+ - run: npm ci
39
+
40
+ - name: Generate map styles
41
+ run: npx tsx node_modules/whereto-bike/scripts/build-map-style.ts
42
+
43
+ - name: Restore Astro content cache
44
+ uses: actions/cache@v5
45
+ with:
46
+ path: .astro
47
+ key: astro-${{ hashFiles('blog/**') }}
48
+ restore-keys: astro-
49
+
50
+ - name: Build site
51
+ run: npx astro build
52
+ env:
53
+ CONTENT_DIR: .
54
+ CITY: blog
55
+ SITE_URL: https://{{DOMAIN}}
56
+ NODE_OPTIONS: '--max-old-space-size=4096'
@@ -0,0 +1,106 @@
1
+ # Auto-generated by `npm run sync` — local changes will be overwritten.
2
+ # Builds and deploys the site to Cloudflare Workers on push to main.
3
+ name: Deploy
4
+
5
+ on:
6
+ push:
7
+ branches: [main]
8
+ workflow_dispatch:
9
+
10
+ concurrency:
11
+ group: deploy
12
+ cancel-in-progress: false
13
+
14
+ jobs:
15
+ deploy:
16
+ runs-on: ubuntu-latest
17
+ environment: production
18
+ steps:
19
+ - uses: actions/checkout@v6
20
+
21
+ - name: LFS cache period
22
+ id: lfs-period
23
+ run: echo "period=$(( $(date +%s) / 1209600 ))" >> "$GITHUB_OUTPUT"
24
+
25
+ - name: Restore LFS cache
26
+ uses: actions/cache@v5
27
+ with:
28
+ path: .git/lfs
29
+ key: lfs-${{ steps.lfs-period.outputs.period }}
30
+ restore-keys: lfs-
31
+
32
+ - name: Pull LFS files
33
+ run: git lfs pull
34
+
35
+ - uses: actions/setup-node@v6
36
+ with:
37
+ node-version: 22
38
+ cache: 'npm'
39
+
40
+ - run: npm ci
41
+
42
+ - name: Record build start time
43
+ run: echo "BUILD_START=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_ENV"
44
+
45
+ - name: Generate map styles
46
+ run: npx tsx node_modules/whereto-bike/scripts/build-map-style.ts
47
+
48
+ - name: Restore map cache
49
+ uses: actions/cache@v5
50
+ with:
51
+ path: public/maps
52
+ key: maps-${{ hashFiles('blog/rides/**/*.gpx', 'package-lock.json') }}
53
+ restore-keys: maps-
54
+
55
+ - name: Restore Astro content cache
56
+ uses: actions/cache@v5
57
+ with:
58
+ path: .astro
59
+ key: astro-${{ hashFiles('blog/**') }}
60
+ restore-keys: astro-
61
+
62
+ - name: Generate map thumbnails
63
+ run: npx tsx node_modules/whereto-bike/scripts/generate-maps.ts
64
+ env:
65
+ CONTENT_DIR: .
66
+ CITY: blog
67
+ GOOGLE_MAPS_STATIC_API_KEY: ${{ secrets.GOOGLE_MAPS_STATIC_API_KEY }}
68
+
69
+ - name: Build site
70
+ run: npx astro build
71
+ env:
72
+ CONTENT_DIR: .
73
+ CITY: blog
74
+ SITE_URL: https://{{DOMAIN}}
75
+ NODE_OPTIONS: '--max-old-space-size=4096'
76
+
77
+ - name: Prepare wrangler config
78
+ run: |
79
+ rm -f dist/server/wrangler.json
80
+ node -e "
81
+ const fs = require('fs');
82
+ const raw = fs.readFileSync('wrangler.jsonc', 'utf8');
83
+ const stripped = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
84
+ const c = JSON.parse(stripped);
85
+ c.main = './dist/server/entry.mjs';
86
+ c.assets.directory = './dist/client';
87
+ fs.writeFileSync('wrangler.jsonc', JSON.stringify(c, null, 2));
88
+ "
89
+
90
+ - name: Run D1 migrations
91
+ run: npx wrangler d1 migrations apply DB --config wrangler.jsonc --remote
92
+ env:
93
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
94
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
95
+
96
+ - name: Deploy to Cloudflare Workers
97
+ run: npx wrangler deploy --config wrangler.jsonc
98
+ env:
99
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
100
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
101
+
102
+ - name: Clear stale content edits cache
103
+ run: npx wrangler d1 execute DB --config wrangler.jsonc --remote --command "DELETE FROM content_edits WHERE updated_at < '${{ env.BUILD_START }}'"
104
+ env:
105
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
106
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
@@ -0,0 +1,59 @@
1
+ # Auto-generated by `npm run sync` — local changes will be overwritten.
2
+ # Weekly: updates whereto-bike, syncs templates, commits and pushes if changed.
3
+ name: Update
4
+
5
+ on:
6
+ schedule:
7
+ - cron: '0 9 * * 1' # Every Monday at 9am UTC
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ update:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v6
15
+
16
+ - name: LFS cache period
17
+ id: lfs-period
18
+ run: echo "period=$(( $(date +%s) / 1209600 ))" >> "$GITHUB_OUTPUT"
19
+
20
+ - name: Restore LFS cache
21
+ uses: actions/cache@v5
22
+ with:
23
+ path: .git/lfs
24
+ key: lfs-${{ steps.lfs-period.outputs.period }}
25
+ restore-keys: lfs-
26
+
27
+ - name: Pull LFS files
28
+ run: git lfs pull
29
+
30
+ - uses: actions/setup-node@v6
31
+ with:
32
+ node-version: 22
33
+ cache: 'npm'
34
+
35
+ - run: npm ci
36
+
37
+ - name: Update whereto-bike
38
+ run: npm update whereto-bike
39
+
40
+ - name: Sync workflow templates
41
+ run: npm run sync
42
+
43
+ - name: Check for changes
44
+ id: changes
45
+ run: |
46
+ if git diff --quiet; then
47
+ echo "updated=false" >> "$GITHUB_OUTPUT"
48
+ else
49
+ echo "updated=true" >> "$GITHUB_OUTPUT"
50
+ fi
51
+
52
+ - name: Commit and push
53
+ if: steps.changes.outputs.updated == 'true'
54
+ run: |
55
+ git config user.name "github-actions[bot]"
56
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
57
+ git add package-lock.json .github/workflows/
58
+ git commit -m "chore: update whereto-bike"
59
+ git push
@@ -0,0 +1,6 @@
1
+ node_modules/
2
+ dist/
3
+ .astro/
4
+ .data/
5
+ .env
6
+ .wrangler/
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "{{FOLDER}}",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "astro dev",
7
+ "build": "astro build",
8
+ "preview": "astro preview",
9
+ "setup": "node scripts/setup.js",
10
+ "sync": "node node_modules/whereto-bike/sync.js"
11
+ },
12
+ "dependencies": {
13
+ "@astrojs/cloudflare": "^13.0.0-beta.11",
14
+ "@astrojs/node": "^10.0.0-beta.6",
15
+ "@astrojs/preact": "^4.1.3",
16
+ "astro": "^6.0.0-beta.20",
17
+ "whereto-bike": "^0.0.1",
18
+ "dotenv": "^17.3.1",
19
+ "preact": "^10.28.4",
20
+ "sass": "^1.97.3"
21
+ }
22
+ }