Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/domain-set-primary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudhal/cli": patch
---

`cloudhal domain set <host>` also makes the host the local site's primary domain, so canonical links, the sitemap and feeds use it before the first deploy (#48).
2 changes: 1 addition & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ Locally, `cloudhal studio` reads the project's SQLite (`analytics_events`), roll

**From Git (Workers Builds).** With `WORKERS_CI=1`, which Cloudflare sets in [Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/), `cloudhal deploy` works from a fresh clone. It takes the names from the committed `wrangler.jsonc`, seeds the starter site when the checkout has none, keeps the secrets on the Workers and does not print the setup link. This is what the Deploy to Cloudflare button runs: see [docs/guides/deploy-button.md](../../docs/guides/deploy-button.md).

**Custom domain.** `cloudhal domain set news.example.com --studio studio.example.com` records the hostnames in `cloudhal.json`; the next `cloudhal deploy` routes them to the Workers as [Cloudflare Custom Domains](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/) (the zone must be in the same Cloudflare account — Cloudflare creates the DNS record and certificate) and makes the site's one its primary hostname, so preview links use it (canonical URLs in the starter are not rendered yet). The workers.dev addresses keep working. `cloudhal domain remove` and a deploy undo it.
**Custom domain.** `cloudhal domain set news.example.com --studio studio.example.com` records the hostnames in `cloudhal.json`; the next `cloudhal deploy` routes them to the Workers as [Cloudflare Custom Domains](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/) (the zone must be in the same Cloudflare account — Cloudflare creates the DNS record and certificate) and makes the site's one its primary hostname on the deployment. `domain set` makes it primary in the local store right away, so `cloudhal dev`, `render` and `build` write it into canonical links, the sitemap and feeds before any deploy. The workers.dev addresses keep working. `cloudhal domain remove` and a deploy undo it.

**Email (RFC-0011).** Add `"mail": { "from": "[email protected]", "fromName": "Example Studio" }` to `cloudhal.json` and the next `cloudhal deploy` gives the admin Worker a [Cloudflare Email Service](https://developers.cloudflare.com/email-service/) `send_email` binding locked to that address. Invites can then be emailed, and staff can reset a forgotten password from the sign-in screen. Onboard the address's domain in Email Service first. Sending to any recipient needs Workers Paid (3,000 emails a month included); on Free, Email Service delivers only to verified destination addresses of your account. Locally, `cloudhal studio` prints these emails in the terminal instead of sending them.

Expand Down
20 changes: 19 additions & 1 deletion packages/cli/src/commands/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@
* `cloudhal domain` — the custom domains `cloudhal deploy` routes to this project's Workers
* (Cloudflare Custom Domains: the zone must be in the same Cloudflare account; wrangler creates
* the DNS record and certificate). Kept in cloudhal.json so the setting travels with the project.
* The site's own hostname also becomes its primary domain in the local store, so `cloudhal build`
* writes it into canonical links, the sitemap and feeds before any deploy (#48); `deploy` does the
* same on the deployment.
*/
import { isHostname, normalizeHostname } from "@cloudhal/core-ts";
import path from "node:path";
import { isHostname, normalizeHostname, withPrimaryDomain } from "@cloudhal/core-ts";
import { openLocalStore } from "@cloudhal/core-ts/local";
import { type ProjectConfig, readProjectConfig, writeProjectConfig } from "../config.ts";

export type DomainTarget = "site" | "studio";
Expand Down Expand Up @@ -40,5 +45,18 @@ export async function domain(options: DomainOptions): Promise<ProjectConfig["dom
...(next.site || next.studio ? { domains: next } : {}),
});
}
if (next.site && options.set?.site) await makePrimary(options.projectDir, cfg, next.site);
return next.site || next.studio ? next : undefined;
}

/** `hostname` becomes the local site's primary domain (other domains stay, as secondary). */
async function makePrimary(projectDir: string, cfg: ProjectConfig, hostname: string) {
const local = await openLocalStore(path.join(projectDir, cfg.dataDir));
try {
const site = await local.store.getSite(cfg.siteId);
if (site && site.domains.find((d) => d.primary)?.hostname !== hostname)
await local.store.updateSite({ ...site, domains: withPrimaryDomain(site.domains, hostname) });
} finally {
await local.close();
}
}
7 changes: 7 additions & 0 deletions packages/cli/test/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,13 @@ describe("cloudhal deploy", () => {
site: "news.example.com",
studio: "studio.example.com",
});
// #48: the local site takes it as its primary domain before any deploy (`cloudhal build`)
const before = await openLocalStore(path.join(projectDir, cfg.dataDir));
expect((await before.store.getSite(cfg.siteId))?.domains[0]).toEqual({
hostname: "news.example.com",
primary: true,
});
await before.close();

const r = await run();
expect(r).toMatchObject({
Expand Down
Loading