create-rush-cms 0.0.2 → 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/LICENSE +21 -0
- package/README.md +58 -2
- package/dist/cli.js +209 -0
- package/package.json +14 -5
- package/template/.env.example +5 -0
- package/template/README.md +127 -0
- package/template/_gitignore +6 -0
- package/template/astro.config.ts +28 -0
- package/template/package.json +28 -0
- package/template/public/favicon.svg +4 -0
- package/template/src/astro-modules.d.ts +5 -0
- package/template/src/components/language-switcher.astro +39 -0
- package/template/src/components/nav-links.astro +33 -0
- package/template/src/components/site-footer.astro +29 -0
- package/template/src/components/site-header.astro +36 -0
- package/template/src/layouts/base.astro +49 -0
- package/template/src/lib/locale.ts +12 -0
- package/template/src/lib/route-page.ts +15 -0
- package/template/src/pages/404.astro +17 -0
- package/template/src/pages/[...locale]/contact.astro +59 -0
- package/template/src/pages/[...path].astro +32 -0
- package/template/src/pages/robots.txt.ts +10 -0
- package/template/src/pages/sitemap.xml.ts +4 -0
- package/template/src/styles/global.css +278 -0
- package/template/src/views/collection.astro +84 -0
- package/template/src/views/entry.astro +65 -0
- package/template/src/views/home.astro +160 -0
- package/template/src/views/index.ts +41 -0
- package/template/src/views/not-found.astro +21 -0
- package/template/tsconfig.json +9 -0
- package/index.js +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rafhael Marsigli
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# create-rush-cms
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Creates a website powered by [Rush CMS](https://github.com/rush-cms/rush-cms): a production-ready
|
|
4
|
+
[Astro](https://astro.build) site wired to [`@rushcms/astro`](https://github.com/rush-cms/js/tree/main/packages/astro)
|
|
5
|
+
and [`@rushcms/client`](https://github.com/rush-cms/js/tree/main/packages/client). It works with any Rush CMS v2 site
|
|
6
|
+
on day one, whatever its collections, locales and blocks. The generated code is yours: `create-rush-cms` never touches
|
|
7
|
+
it again.
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
Requires Node 24 or later.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm create rush-cms@latest my-site
|
|
15
|
+
bun create rush-cms my-site
|
|
16
|
+
pnpm create rush-cms my-site
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
| Argument or flag | Description |
|
|
20
|
+
| ---------------- | ------------------------------------------------------------- |
|
|
21
|
+
| `<dir>` | Required. The folder to create; it must not exist or be empty |
|
|
22
|
+
| `--no-install` | Skip installing dependencies |
|
|
23
|
+
| `-h`, `--help` | Print the usage and exit |
|
|
24
|
+
|
|
25
|
+
A missing `<dir>`, an unknown flag or an extra argument prints the usage and exits with code 1. An existing folder
|
|
26
|
+
that is not empty exits with code 1 and nothing is written.
|
|
27
|
+
|
|
28
|
+
## What it writes
|
|
29
|
+
|
|
30
|
+
- A copy of the [model site](https://github.com/rush-cms/js/tree/main/templates/astro) with `@rushcms/client` and
|
|
31
|
+
`@rushcms/astro` pinned to the exact versions released together with this package.
|
|
32
|
+
- `package.json` `name`, taken from the folder name (made npm-valid).
|
|
33
|
+
- `.gitignore` (stored as `_gitignore` in the package, because npm strips `.gitignore` from tarballs).
|
|
34
|
+
- `.env`, a copy of `.env.example` with empty values.
|
|
35
|
+
- `node_modules`, installed with the package manager that ran the command (npm, pnpm, bun or yarn; npm when it cannot
|
|
36
|
+
tell) unless `--no-install` is given. When the install fails, the files stay and the command to run by hand is
|
|
37
|
+
printed, with exit code 1.
|
|
38
|
+
|
|
39
|
+
## What it never does
|
|
40
|
+
|
|
41
|
+
- Ask questions: there are no prompts.
|
|
42
|
+
- Use the network, apart from the dependency install.
|
|
43
|
+
- Read, ask for or write secrets: `.env` is left empty for you to fill.
|
|
44
|
+
- Run `git init`, commit or touch an existing repository.
|
|
45
|
+
- Edit anything after the first run.
|
|
46
|
+
|
|
47
|
+
## Next steps
|
|
48
|
+
|
|
49
|
+
1. `cd my-site`, and install if you used `--no-install`.
|
|
50
|
+
2. Fill `.env` with the API URL, the site id and a delivery token of your Rush CMS site.
|
|
51
|
+
3. Run `npm run dev` (or `bun run dev`, `pnpm run dev`). The first start stops with an error that lists the site's
|
|
52
|
+
locales and a `localePaths` line; paste it into `astro.config.ts` and run it again.
|
|
53
|
+
4. Search the project for `TODO` markers: each one is a place to customize (routes, navigation, the production
|
|
54
|
+
domain, `trustedProxies`).
|
|
55
|
+
|
|
56
|
+
The [template README](https://github.com/rush-cms/js/blob/main/templates/astro/README.md) covers the routes, views,
|
|
57
|
+
preview deployment and deployment topologies.
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, join, resolve } from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
// src/copy.ts
|
|
7
|
+
const GITIGNORE_SOURCE = "_gitignore";
|
|
8
|
+
const GITIGNORE_TARGET = ".gitignore";
|
|
9
|
+
const TEMPLATE_RULES = {
|
|
10
|
+
skip: () => false,
|
|
11
|
+
rename: (name) => name === GITIGNORE_SOURCE ? GITIGNORE_TARGET : name
|
|
12
|
+
};
|
|
13
|
+
function copyTree(from, to, rules) {
|
|
14
|
+
mkdirSync(to, { recursive: true });
|
|
15
|
+
for (const entry of readdirSync(from, { withFileTypes: true })) {
|
|
16
|
+
if (rules.skip(entry.name)) continue;
|
|
17
|
+
const source = join(from, entry.name);
|
|
18
|
+
const target = join(to, rules.rename(entry.name));
|
|
19
|
+
if (entry.isDirectory()) copyTree(source, target, rules);
|
|
20
|
+
else copyFileSync(source, target);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function copyTemplate(from, to) {
|
|
24
|
+
copyTree(from, to, TEMPLATE_RULES);
|
|
25
|
+
}
|
|
26
|
+
// src/package-manager.ts
|
|
27
|
+
const KNOWN = [
|
|
28
|
+
"pnpm",
|
|
29
|
+
"bun",
|
|
30
|
+
"yarn"
|
|
31
|
+
];
|
|
32
|
+
function detectPackageManager(userAgent) {
|
|
33
|
+
const launcher = userAgent?.split(" ")[0]?.split("/")[0];
|
|
34
|
+
return KNOWN.find((name) => name === launcher) ?? "npm";
|
|
35
|
+
}
|
|
36
|
+
function installCommand(packageManager) {
|
|
37
|
+
return `${packageManager} install`;
|
|
38
|
+
}
|
|
39
|
+
function devCommand(packageManager) {
|
|
40
|
+
return `${packageManager} run dev`;
|
|
41
|
+
}
|
|
42
|
+
// src/next-steps.ts
|
|
43
|
+
function quoted(dir) {
|
|
44
|
+
return /\s/.test(dir) ? JSON.stringify(dir) : dir;
|
|
45
|
+
}
|
|
46
|
+
const USAGE = [
|
|
47
|
+
"Usage: create-rush-cms <dir> [--no-install]",
|
|
48
|
+
"",
|
|
49
|
+
"Creates a Rush CMS website in <dir>.",
|
|
50
|
+
"",
|
|
51
|
+
"Options:",
|
|
52
|
+
" --no-install Skip installing dependencies",
|
|
53
|
+
" -h, --help Show this help"
|
|
54
|
+
];
|
|
55
|
+
function installFailure(input) {
|
|
56
|
+
return ["Dependency installation failed. The project files were kept.", `Install manually: cd ${quoted(input.dir)} && ${installCommand(input.packageManager)}`];
|
|
57
|
+
}
|
|
58
|
+
function nextSteps(input) {
|
|
59
|
+
return [
|
|
60
|
+
"",
|
|
61
|
+
"Next steps:",
|
|
62
|
+
...[
|
|
63
|
+
`cd ${quoted(input.dir)}`,
|
|
64
|
+
...input.installed ? [] : [installCommand(input.packageManager)],
|
|
65
|
+
"Fill .env with your Rush CMS API URL, site id and delivery token",
|
|
66
|
+
`${devCommand(input.packageManager)}, then paste the localePaths line the first-run error prints into astro.config.ts`,
|
|
67
|
+
"Search the project for TODO markers and fill them in"
|
|
68
|
+
].map((step, index) => ` ${index + 1}. ${step}`)
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
// src/manifest.ts
|
|
72
|
+
const DEFAULT_INDENT = " ";
|
|
73
|
+
function updateManifest(file, update) {
|
|
74
|
+
const text = readFileSync(file, "utf8");
|
|
75
|
+
const indent = /\n([ \t]+)"/.exec(text)?.[1] ?? DEFAULT_INDENT;
|
|
76
|
+
const manifest = JSON.parse(text);
|
|
77
|
+
update(manifest);
|
|
78
|
+
writeFileSync(file, `${JSON.stringify(manifest, null, indent)}\n`);
|
|
79
|
+
}
|
|
80
|
+
// src/package-name.ts
|
|
81
|
+
const FALLBACK = "rush-site";
|
|
82
|
+
const MAX_LENGTH = 214;
|
|
83
|
+
const RESERVED = /* @__PURE__ */ new Set(["node_modules", "favicon.ico"]);
|
|
84
|
+
function packageNameOf(dir) {
|
|
85
|
+
const name = basename(resolve(dir)).toLowerCase().normalize("NFD").replaceAll(/\p{M}/gu, "").replaceAll(/[^a-z0-9._~-]+/g, "-").replace(/^[._-]+/, "").slice(0, MAX_LENGTH).replace(/-+$/, "");
|
|
86
|
+
return name === "" || RESERVED.has(name) ? FALLBACK : name;
|
|
87
|
+
}
|
|
88
|
+
// src/run.ts
|
|
89
|
+
const DEFAULT_TEMPLATE_DIR = resolve(import.meta.dirname, "..", "template");
|
|
90
|
+
function messageOf(error) {
|
|
91
|
+
return error instanceof Error ? error.message : String(error);
|
|
92
|
+
}
|
|
93
|
+
function emit(lines, write) {
|
|
94
|
+
for (const line of lines) write(line);
|
|
95
|
+
}
|
|
96
|
+
function parse(argv) {
|
|
97
|
+
try {
|
|
98
|
+
const { values, positionals } = parseArgs({
|
|
99
|
+
args: argv,
|
|
100
|
+
allowPositionals: true,
|
|
101
|
+
options: {
|
|
102
|
+
help: {
|
|
103
|
+
type: "boolean",
|
|
104
|
+
short: "h"
|
|
105
|
+
},
|
|
106
|
+
"no-install": { type: "boolean" }
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
const [dir, ...extra] = positionals;
|
|
110
|
+
if (values.help === true) return { kind: "help" };
|
|
111
|
+
if (dir === void 0) return {
|
|
112
|
+
kind: "invalid",
|
|
113
|
+
message: "Missing <dir>."
|
|
114
|
+
};
|
|
115
|
+
if (extra.length > 0) return {
|
|
116
|
+
kind: "invalid",
|
|
117
|
+
message: `Unexpected argument: ${extra.join(" ")}`
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
kind: "ready",
|
|
121
|
+
options: {
|
|
122
|
+
dir,
|
|
123
|
+
install: values["no-install"] !== true
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return {
|
|
128
|
+
kind: "invalid",
|
|
129
|
+
message: messageOf(error)
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function isNonEmpty(target) {
|
|
134
|
+
return existsSync(target) && (!statSync(target).isDirectory() || readdirSync(target).length > 0);
|
|
135
|
+
}
|
|
136
|
+
function setPackageName(target) {
|
|
137
|
+
updateManifest(join(target, "package.json"), (manifest) => {
|
|
138
|
+
manifest.name = packageNameOf(target);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function scaffold(target, templateDir) {
|
|
142
|
+
copyTemplate(templateDir, target);
|
|
143
|
+
setPackageName(target);
|
|
144
|
+
copyFileSync(join(target, ".env.example"), join(target, ".env"));
|
|
145
|
+
}
|
|
146
|
+
async function run(argv, env, io, templateDir = DEFAULT_TEMPLATE_DIR) {
|
|
147
|
+
const parsed = parse(argv);
|
|
148
|
+
if (parsed.kind === "help") {
|
|
149
|
+
emit(USAGE, io.out);
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
if (parsed.kind === "invalid") {
|
|
153
|
+
io.err(parsed.message);
|
|
154
|
+
emit(USAGE, io.err);
|
|
155
|
+
return 1;
|
|
156
|
+
}
|
|
157
|
+
const { dir, install } = parsed.options;
|
|
158
|
+
const target = resolve(env.cwd, dir);
|
|
159
|
+
if (isNonEmpty(target)) {
|
|
160
|
+
io.err(`${dir} already exists and is not empty. Nothing was written.`);
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
scaffold(target, templateDir);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
io.err(`Could not create the project: ${messageOf(error)}`);
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
const packageManager = detectPackageManager(env.userAgent);
|
|
170
|
+
const input = {
|
|
171
|
+
dir,
|
|
172
|
+
packageManager,
|
|
173
|
+
installed: install
|
|
174
|
+
};
|
|
175
|
+
io.out(`Created ${packageNameOf(target)} in ${dir}`);
|
|
176
|
+
if (install && await io.spawn(packageManager, target) !== 0) {
|
|
177
|
+
emit(installFailure(input), io.err);
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
emit(nextSteps(input), io.out);
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
// src/spawn-install.ts
|
|
184
|
+
const FAILURE = 1;
|
|
185
|
+
function runCommand(command, args, cwd) {
|
|
186
|
+
return new Promise((resolve) => {
|
|
187
|
+
const child = spawn(command, args, {
|
|
188
|
+
cwd,
|
|
189
|
+
stdio: "inherit",
|
|
190
|
+
shell: process.platform === "win32"
|
|
191
|
+
});
|
|
192
|
+
child.on("error", () => resolve(FAILURE));
|
|
193
|
+
child.on("close", (code) => resolve(code ?? FAILURE));
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function spawnInstall(packageManager, cwd) {
|
|
197
|
+
const [command = packageManager, ...args] = installCommand(packageManager).split(" ");
|
|
198
|
+
return runCommand(command, args, cwd);
|
|
199
|
+
}
|
|
200
|
+
// src/cli.ts
|
|
201
|
+
process.exitCode = await run(process.argv.slice(2), {
|
|
202
|
+
userAgent: process.env.npm_config_user_agent,
|
|
203
|
+
cwd: process.cwd()
|
|
204
|
+
}, {
|
|
205
|
+
out: (line) => console.log(line),
|
|
206
|
+
err: (line) => console.error(line),
|
|
207
|
+
spawn: spawnInstall
|
|
208
|
+
});
|
|
209
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-rush-cms",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "Create
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Create an Astro website powered by Rush CMS.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"create-rush-cms": "
|
|
7
|
+
"create-rush-cms": "dist/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"
|
|
10
|
+
"dist",
|
|
11
|
+
"template"
|
|
11
12
|
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=24"
|
|
15
|
+
},
|
|
12
16
|
"repository": {
|
|
13
17
|
"type": "git",
|
|
14
18
|
"url": "git+https://github.com/rush-cms/js.git",
|
|
@@ -22,5 +26,10 @@
|
|
|
22
26
|
"starter"
|
|
23
27
|
],
|
|
24
28
|
"author": "Rafhael Marsigli <oi@rafhael.com.br>",
|
|
25
|
-
"license": "MIT"
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsdown && bun ../../scripts/bundle-template.ts",
|
|
32
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
33
|
+
"test": "vitest run"
|
|
34
|
+
}
|
|
26
35
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Rush CMS site
|
|
2
|
+
|
|
3
|
+
An [Astro](https://astro.build) site wired to [Rush CMS](https://github.com/rush-cms/rush-cms) through
|
|
4
|
+
[`@rushcms/astro`](https://github.com/rush-cms/js/tree/main/packages/astro) and
|
|
5
|
+
[`@rushcms/client`](https://github.com/rush-cms/js/tree/main/packages/client). It works with any Rush CMS site: every
|
|
6
|
+
collection, entry, locale, navigation and form comes from the CMS, and the URLs come from this repository. The code
|
|
7
|
+
is yours now; change anything.
|
|
8
|
+
|
|
9
|
+
Requires Node 24 or later.
|
|
10
|
+
|
|
11
|
+
## First run
|
|
12
|
+
|
|
13
|
+
1. Fill `.env`:
|
|
14
|
+
|
|
15
|
+
| Variable | Value |
|
|
16
|
+
| ---------------- | ----------------------------------------------------------- |
|
|
17
|
+
| `RUSH_API_URL` | The Rush CMS API base URL |
|
|
18
|
+
| `RUSH_SITE_ID` | The site ULID |
|
|
19
|
+
| `RUSH_API_TOKEN` | A delivery token of the site (secret: never commit it) |
|
|
20
|
+
| `RUSH_MEDIA_URL` | The media host, when it differs from the API (optional) |
|
|
21
|
+
| `RUSH_PREVIEW` | `true` only for a [preview deployment](#preview-deployment) |
|
|
22
|
+
|
|
23
|
+
2. Run `npm run dev`. The first start stops with an error that lists the site's locales and a line to paste:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
@rushcms/astro: localePaths locales [] differ from the site locales [pt_BR, en]. Set in astro.config:
|
|
27
|
+
localePaths: { pt_BR: '', en: 'en' }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
When an AI coding agent starts the dev server, Astro runs it in the background and writes this error to
|
|
31
|
+
`.astro/dev.log` instead of the terminal.
|
|
32
|
+
|
|
33
|
+
3. Paste that line over `localePaths: {}` in `astro.config.ts` and run `npm run dev` again. The default locale has
|
|
34
|
+
no URL prefix; every other locale gets its own. Change the prefixes now if you like: they are part of your URLs.
|
|
35
|
+
|
|
36
|
+
4. Open <http://localhost:4321>, then work through the `TODO` comments (your editor's TODO list finds them all).
|
|
37
|
+
|
|
38
|
+
| Command | What it does |
|
|
39
|
+
| ----------------- | --------------------------------------------------------------------- |
|
|
40
|
+
| `npm run dev` | Development server with live content |
|
|
41
|
+
| `npm run build` | Production build in `dist/` |
|
|
42
|
+
| `npm run preview` | Runs the built server with the variables of `.env`, when there is one |
|
|
43
|
+
| `npm run check` | Type-checks the project with `astro check` |
|
|
44
|
+
|
|
45
|
+
## Routes and views
|
|
46
|
+
|
|
47
|
+
URLs come only from `routes` and `localePaths` in `astro.config.ts`. The template ships one generic route,
|
|
48
|
+
`'*': '/{locale}/{collection}/{slug}'`, which gives every collection a list page (`/en/posts`, `/en/posts/page/2`)
|
|
49
|
+
and entry pages (`/en/posts/hello`). Replace `'*'` with one key per public collection (a collection id or slug) so
|
|
50
|
+
that collections holding only data get no pages. A collection without a route has no pages, no sitemap entries and
|
|
51
|
+
no navigation links. See [Routes](https://github.com/rush-cms/js/tree/main/packages/astro#routes) for the rules.
|
|
52
|
+
|
|
53
|
+
No page file encodes a URL shape:
|
|
54
|
+
|
|
55
|
+
| File | Answers |
|
|
56
|
+
| ------------------------------------- | ----------------------------------------------------------------------------- |
|
|
57
|
+
| `src/pages/[...path].astro` | Every homepage, collection list page and entry, prerendered at build time |
|
|
58
|
+
| `src/pages/404.astro` | Every other path, on demand: redirects retired slugs, else the not-found page |
|
|
59
|
+
| `src/pages/[...locale]/contact.astro` | The contact form of each locale, on demand |
|
|
60
|
+
| `src/pages/sitemap.xml.ts` | The sitemap, from the same routes |
|
|
61
|
+
| `src/pages/robots.txt.ts` | `robots.txt` (everything disallowed in a preview deployment) |
|
|
62
|
+
|
|
63
|
+
The dispatcher hands each page to a view:
|
|
64
|
+
|
|
65
|
+
- `src/views/home.astro`: the front page of every locale. It is a static welcome page; replace it with your design,
|
|
66
|
+
or render the homepage built in Rush CMS from `page.homepage`.
|
|
67
|
+
- `src/views/collection.astro`: a collection list page with pagination.
|
|
68
|
+
- `src/views/entry.astro`: an entry with its title, featured image, excerpt and every rich-text field.
|
|
69
|
+
- `src/views/not-found.astro`: the 404 page.
|
|
70
|
+
|
|
71
|
+
To give a collection its own design, create a view and map the collection id or slug in `src/views/index.ts`:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import PostEntry from './post-entry.astro'
|
|
75
|
+
|
|
76
|
+
const views: ViewMap = { posts: { entry: PostEntry } }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A view receives the same props as the generic one (`EntryViewProps` or `ListViewProps`), and `npm run check` fails
|
|
80
|
+
when it does not. `src/astro-modules.d.ts` lets plain TypeScript read the `.astro` imports of that file.
|
|
81
|
+
|
|
82
|
+
## Navigation and forms
|
|
83
|
+
|
|
84
|
+
Site content is fetched by stable keys that you create in Rush CMS:
|
|
85
|
+
|
|
86
|
+
| Key | Kind | Used by |
|
|
87
|
+
| --------- | ---------- | ------------------------------------------------------ |
|
|
88
|
+
| `main` | Navigation | The header menu (nested items open on hover and focus) |
|
|
89
|
+
| `footer` | Navigation | The footer menu |
|
|
90
|
+
| `contact` | Form | `/contact` (and `/<locale>/contact`) |
|
|
91
|
+
|
|
92
|
+
Missing content renders nothing: no menu without the navigation, and `/contact` answers 404 without the form (in
|
|
93
|
+
`npm run dev` it shows a reminder instead). Change a key where the `TODO` points to it.
|
|
94
|
+
|
|
95
|
+
## Preview deployment
|
|
96
|
+
|
|
97
|
+
Deploy the same repository a second time with `RUSH_PREVIEW=true` and point the CMS preview URL templates at it:
|
|
98
|
+
|
|
99
|
+
```text
|
|
100
|
+
https://preview.example.com/_rush/preview?token={token}&locale={locale}&collection={collection}&slug={slug}
|
|
101
|
+
https://preview.example.com/_rush/preview?token={token}&locale={locale}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Every page of that deployment is rendered on demand with drafts, carries `X-Robots-Tag: noindex`, and shows a banner
|
|
105
|
+
with a link to leave the preview. See
|
|
106
|
+
[Preview deployment](https://github.com/rush-cms/js/tree/main/packages/astro#preview-deployment).
|
|
107
|
+
|
|
108
|
+
## Deployment
|
|
109
|
+
|
|
110
|
+
- `npm run build` fetches every page from the API and writes `dist/`. Static pages change only when you build again,
|
|
111
|
+
so rebuild after publishing content.
|
|
112
|
+
- Run the server with `node ./dist/server/entry.mjs` (`HOST` and `PORT` set where it listens). It reads the `RUSH_`
|
|
113
|
+
variables from the process environment, not from `.env`: set them on your host. `npm run preview` starts it
|
|
114
|
+
locally with `.env` loaded (`node --env-file-if-exists=.env ./dist/server/entry.mjs`). The server answers the 404
|
|
115
|
+
page, redirects, the contact page and form posts (`/_rush/forms/<key>`).
|
|
116
|
+
- Set `site` to your production URL: canonical links, `hreflang` links and the sitemap use it.
|
|
117
|
+
- List your domains in `security.allowedDomains`, or form posts and preview links fail on them.
|
|
118
|
+
- Set `trustedProxies` to the number of reverse proxies in front of the server, so form posts are rate-limited per
|
|
119
|
+
visitor. The [topology table](https://github.com/rush-cms/js/tree/main/packages/astro#behind-reverse-proxies) gives
|
|
120
|
+
the value for Coolify, nginx and Cloudflare.
|
|
121
|
+
|
|
122
|
+
## Styling
|
|
123
|
+
|
|
124
|
+
[Tailwind CSS 4](https://tailwindcss.com) through `@tailwindcss/vite`. The design tokens (colors and fonts) live in
|
|
125
|
+
`@theme` in `src/styles/global.css`, with their dark values under `prefers-color-scheme: dark`, so changing a token
|
|
126
|
+
restyles both themes. Rich text is styled by `@tailwindcss/typography` (`prose`), mapped to the same tokens. The UI
|
|
127
|
+
texts of the views are in English; translate them where they appear.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import node from '@astrojs/node'
|
|
2
|
+
import rush from '@rushcms/astro'
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
4
|
+
import { defineConfig } from 'astro/config'
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
// TODO: set your production URL; canonical links, hreflang links and the sitemap are built from it.
|
|
8
|
+
site: 'http://localhost:4321',
|
|
9
|
+
output: 'static',
|
|
10
|
+
adapter: node({ mode: 'standalone' }),
|
|
11
|
+
security: {
|
|
12
|
+
// TODO: list every domain that serves this site, or form posts and preview links fail there.
|
|
13
|
+
allowedDomains: [{ protocol: 'https', hostname: 'www.example.com' }]
|
|
14
|
+
},
|
|
15
|
+
vite: {
|
|
16
|
+
plugins: [tailwindcss()]
|
|
17
|
+
},
|
|
18
|
+
integrations: [
|
|
19
|
+
rush({
|
|
20
|
+
// TODO: paste the localePaths line that the first `npm run dev` prints.
|
|
21
|
+
localePaths: {},
|
|
22
|
+
// TODO: replace '*' with one route per public collection, so data-only collections get no pages.
|
|
23
|
+
routes: { '*': '/{locale}/{collection}/{slug}' },
|
|
24
|
+
// TODO: set how many reverse proxies sit in front of the server (see "Deployment" in README.md).
|
|
25
|
+
trustedProxies: 1
|
|
26
|
+
})
|
|
27
|
+
]
|
|
28
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rush-cms-template-astro",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=24"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "astro dev",
|
|
11
|
+
"build": "astro build",
|
|
12
|
+
"preview": "node --env-file-if-exists=.env ./dist/server/entry.mjs",
|
|
13
|
+
"check": "astro check"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@astrojs/node": "11.1.11",
|
|
17
|
+
"@rushcms/astro": "0.2.0",
|
|
18
|
+
"@rushcms/client": "4.0.0",
|
|
19
|
+
"@tailwindcss/typography": "0.5.20",
|
|
20
|
+
"@tailwindcss/vite": "4.3.5",
|
|
21
|
+
"astro": "7.4.7",
|
|
22
|
+
"tailwindcss": "4.3.5"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@astrojs/check": "0.9.12",
|
|
26
|
+
"typescript": "npm:@typescript/typescript6@6.0.2"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getUrls, homepageAlternates } from '@rushcms/astro'
|
|
3
|
+
import type { Alternate, LocaleCode } from '@rushcms/client'
|
|
4
|
+
import { languageName } from '../lib/locale'
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
locale: LocaleCode
|
|
8
|
+
translations: Alternate[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { locale, translations } = Astro.props
|
|
12
|
+
const links = homepageAlternates(getUrls(Astro)).map((home) => ({
|
|
13
|
+
...home,
|
|
14
|
+
label: languageName(home.locale),
|
|
15
|
+
url: translations.find((translation) => translation.locale === home.locale)?.url ?? home.url
|
|
16
|
+
}))
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
links.length > 1 && (
|
|
21
|
+
<nav aria-label="Language" class="text-sm">
|
|
22
|
+
<ul class="flex flex-wrap items-center gap-1">
|
|
23
|
+
{links.map((link) => (
|
|
24
|
+
<li>
|
|
25
|
+
<a
|
|
26
|
+
href={link.url}
|
|
27
|
+
hreflang={link.hreflang}
|
|
28
|
+
lang={link.hreflang}
|
|
29
|
+
aria-current={link.locale === locale ? 'true' : undefined}
|
|
30
|
+
class="block rounded-full px-3 py-1 text-muted transition-colors hover:text-ink aria-[current]:bg-accent-soft aria-[current]:text-ink"
|
|
31
|
+
>
|
|
32
|
+
{link.label}
|
|
33
|
+
</a>
|
|
34
|
+
</li>
|
|
35
|
+
))}
|
|
36
|
+
</ul>
|
|
37
|
+
</nav>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { NavigationLink } from '@rushcms/client'
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
links: NavigationLink[]
|
|
6
|
+
nested?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { links, nested = false } = Astro.props
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<ul class:list={['nav-links', nested ? 'nav-links-nested' : 'nav-links-top']}>
|
|
13
|
+
{
|
|
14
|
+
links.map((link) => (
|
|
15
|
+
<li class="nav-item">
|
|
16
|
+
{link.href === undefined ? (
|
|
17
|
+
<span class="nav-label">{link.label}</span>
|
|
18
|
+
) : (
|
|
19
|
+
<a
|
|
20
|
+
{...link.attributes}
|
|
21
|
+
href={link.href}
|
|
22
|
+
target={link.target}
|
|
23
|
+
rel={link.rel}
|
|
24
|
+
class:list={['nav-link', link.className]}
|
|
25
|
+
>
|
|
26
|
+
{link.label}
|
|
27
|
+
</a>
|
|
28
|
+
)}
|
|
29
|
+
{link.children.length > 0 && <Astro.self links={link.children} nested />}
|
|
30
|
+
</li>
|
|
31
|
+
))
|
|
32
|
+
}
|
|
33
|
+
</ul>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { NavigationLink } from '@rushcms/client'
|
|
3
|
+
import NavLinks from './nav-links.astro'
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
name: string
|
|
7
|
+
links: NavigationLink[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { name, links } = Astro.props
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<footer class="border-t border-line">
|
|
14
|
+
<div
|
|
15
|
+
class="mx-auto flex max-w-6xl flex-col gap-6 px-6 py-10 text-sm text-muted md:flex-row md:items-start md:justify-between"
|
|
16
|
+
>
|
|
17
|
+
<p>
|
|
18
|
+
© {`${new Date().getFullYear()} ${name}`}. Powered by{' '}
|
|
19
|
+
<span class="font-medium text-ink">Rush CMS</span>.
|
|
20
|
+
</p>
|
|
21
|
+
{
|
|
22
|
+
links.length > 0 && (
|
|
23
|
+
<nav aria-label="Footer" class="footer-nav">
|
|
24
|
+
<NavLinks links={links} />
|
|
25
|
+
</nav>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
</div>
|
|
29
|
+
</footer>
|