create-rush-cms 0.0.2 → 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/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
- Scaffolds a website powered by [Rush CMS](https://github.com/rush-cms).
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
- Not released yet. Follow https://github.com/rush-cms/js
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.2",
4
- "description": "Create a website powered by Rush CMS. Not released yet.",
3
+ "version": "0.2.0",
4
+ "description": "Create an Astro website powered by Rush CMS.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "create-rush-cms": "index.js"
7
+ "create-rush-cms": "dist/cli.js"
8
8
  },
9
9
  "files": [
10
- "index.js"
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,5 @@
1
+ RUSH_API_URL=
2
+ RUSH_SITE_ID=
3
+ RUSH_API_TOKEN=
4
+ RUSH_MEDIA_URL=
5
+ RUSH_PREVIEW=
@@ -0,0 +1,148 @@
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, list page, taxonomy listing 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/taxonomy.astro`: the entries of a category or tag, with pagination (see [Taxonomies](#taxonomies)).
69
+ - `src/views/entry.astro`: an entry with its title, featured image, excerpt and every rich-text field.
70
+ - `src/views/not-found.astro`: the 404 page.
71
+
72
+ To give a collection its own design, create a view and map the collection id or slug in `src/views/index.ts`:
73
+
74
+ ```ts
75
+ import PostEntry from './post-entry.astro'
76
+
77
+ const views: ViewMap = { posts: { entry: PostEntry } }
78
+ ```
79
+
80
+ A view receives the same props as the generic one (`EntryViewProps` or `ListViewProps`), and `npm run check` fails
81
+ when it does not. `src/astro-modules.d.ts` lets plain TypeScript read the `.astro` imports of that file.
82
+
83
+ ## Taxonomies
84
+
85
+ Categories and tags have no pages until `taxonomies` in `astro.config.ts` gives their type a path segment in every
86
+ locale of `localePaths`:
87
+
88
+ ```ts
89
+ taxonomies: {
90
+ category: { en: 'category', pt_BR: 'categoria' },
91
+ tag: { en: 'tag', pt_BR: 'tag' }
92
+ }
93
+ ```
94
+
95
+ Each category or tag with entries then gets a listing page per locale (`/category/design`, `/pt/categoria/design`,
96
+ `/category/design/page/2`), rendered by `src/views/taxonomy.astro` (`TaxonomyViewProps`) and listed in the sitemap.
97
+ The language switcher and the `hreflang` links of a listing point at the same category or tag in the other locales
98
+ (`page.alternates`). A locale where it has no entries gets no `hreflang` link, and its switcher link goes to its
99
+ homepage.
100
+ Link to one with `getUrls(Astro).taxonomy(tag, locale)`, for instance from an entry's `categories`. See
101
+ [Taxonomies](https://github.com/rush-cms/js/tree/main/packages/astro#taxonomies) for the rules.
102
+
103
+ ## Navigation and forms
104
+
105
+ Site content is fetched by stable keys that you create in Rush CMS:
106
+
107
+ | Key | Kind | Used by |
108
+ | --------- | ---------- | ------------------------------------------------------ |
109
+ | `main` | Navigation | The header menu (nested items open on hover and focus) |
110
+ | `footer` | Navigation | The footer menu |
111
+ | `contact` | Form | `/contact` (and `/<locale>/contact`) |
112
+
113
+ Missing content renders nothing: no menu without the navigation, and `/contact` answers 404 without the form (in
114
+ `npm run dev` it shows a reminder instead). Change a key where the `TODO` points to it.
115
+
116
+ ## Preview deployment
117
+
118
+ Deploy the same repository a second time with `RUSH_PREVIEW=true` and point the CMS preview URL templates at it:
119
+
120
+ ```text
121
+ https://preview.example.com/_rush/preview?token={token}&locale={locale}&collection={collection}&slug={slug}
122
+ https://preview.example.com/_rush/preview?token={token}&locale={locale}
123
+ ```
124
+
125
+ Every page of that deployment is rendered on demand with drafts, carries `X-Robots-Tag: noindex`, and shows a banner
126
+ with a link to leave the preview. See
127
+ [Preview deployment](https://github.com/rush-cms/js/tree/main/packages/astro#preview-deployment).
128
+
129
+ ## Deployment
130
+
131
+ - `npm run build` fetches every page from the API and writes `dist/`. Static pages change only when you build again,
132
+ so rebuild after publishing content.
133
+ - Run the server with `node ./dist/server/entry.mjs` (`HOST` and `PORT` set where it listens). It reads the `RUSH_`
134
+ variables from the process environment, not from `.env`: set them on your host. `npm run preview` starts it
135
+ locally with `.env` loaded (`node --env-file-if-exists=.env ./dist/server/entry.mjs`). The server answers the 404
136
+ page, redirects, the contact page and form posts (`/_rush/forms/<key>`).
137
+ - Set `site` to your production URL: canonical links, `hreflang` links and the sitemap use it.
138
+ - List your domains in `security.allowedDomains`, or form posts and preview links fail on them.
139
+ - Set `trustedProxies` to the number of reverse proxies in front of the server, so form posts are rate-limited per
140
+ visitor. The [topology table](https://github.com/rush-cms/js/tree/main/packages/astro#behind-reverse-proxies) gives
141
+ the value for Coolify, nginx and Cloudflare.
142
+
143
+ ## Styling
144
+
145
+ [Tailwind CSS 4](https://tailwindcss.com) through `@tailwindcss/vite`. The design tokens (colors and fonts) live in
146
+ `@theme` in `src/styles/global.css`, with their dark values under `prefers-color-scheme: dark`, so changing a token
147
+ restyles both themes. Rich text is styled by `@tailwindcss/typography` (`prose`), mapped to the same tokens. The UI
148
+ texts of the views are in English; translate them where they appear.
@@ -0,0 +1,6 @@
1
+ node_modules
2
+ dist
3
+ .astro
4
+ .env
5
+ .env.*
6
+ !.env.example
@@ -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.3.0",
18
+ "@rushcms/client": "4.1.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,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <rect width="32" height="32" rx="8" fill="#5b4bdb" />
3
+ <path d="M10 8h7.5a5.5 5.5 0 0 1 1.9 10.66L23 24h-4.4l-3.1-4.8H14V24h-4zm4 3.4v4.4h3.3a2.2 2.2 0 0 0 0-4.4z" fill="#fff" />
4
+ </svg>
@@ -0,0 +1,5 @@
1
+ declare module '*.astro' {
2
+ type AstroComponent = (props: Record<string, unknown>) => unknown
3
+ const component: AstroComponent
4
+ export default component
5
+ }
@@ -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
+ }