plum-cms 0.2.2 → 0.2.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +71 -0
- data/README.md +5 -1
- data/app/views/layouts/plum/cp.html.erb +1 -1
- data/app/views/layouts/plum/session.html.erb +1 -1
- data/app/views/layouts/plum/write.html.erb +1 -1
- data/docs/plum-cli.md +66 -0
- data/docs/zero-to-agency.md +172 -0
- data/lib/generators/plum/install/install_generator.rb +13 -1
- data/lib/plum/engine.rb +11 -4
- data/lib/plum/version.rb +1 -1
- data/lib/plum.rb +7 -0
- metadata +11 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a62b9b90b91530846a18cac552fcc7ee10cafcd67ef23ea135d12ff0d175f7e9
|
|
4
|
+
data.tar.gz: 7eada3af7d1000b9dbd526b3b7eab29af60de2790fad7cb7d5ddcb56146c8a68
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f038e184d32a8fb9bb1c0449b7f67328d895f2974e0e71fd7e88d5b0f24be5329ad9c348d9d86e9115d0415507fb9366ac3988d121894691d23741d036e96ee7
|
|
7
|
+
data.tar.gz: eb22b20f634d5ff40d715c53c0c07d31109785305ed27dd6989fd44e923b89a6509efc4ff7289bb9aacf0788c008301c747cf088a888381510af9678e8bb8b04
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- `plum deploy`'s health wait no longer requires a Dockerfile HEALTHCHECK:
|
|
6
|
+
images without one are probed via HTTP /up through the proxy instead
|
|
7
|
+
(resolved to loopback so SNI routing matches).
|
|
8
|
+
- Added `plum promote`: go-live for a preview, in place. The preview app
|
|
9
|
+
is re-homed onto the production domain via `once update --host` — same
|
|
10
|
+
container, same database, all content built during the preview — with
|
|
11
|
+
TLS provisioned on the spot. Preflights preview existence, hostname
|
|
12
|
+
collisions, and DNS. Verified live: a hostname re-home round-trip on a
|
|
13
|
+
running app served on the new domain within seconds, data intact.
|
|
14
|
+
- Added `plum deploy --preview` — the agency preview workflow: ship the
|
|
15
|
+
same image as a separate Once app under a preview hostname, shareable
|
|
16
|
+
with a client before their domain points anywhere. Uses `preview_host:`
|
|
17
|
+
from plum.yml (wildcard-record agency domains) or auto-derives
|
|
18
|
+
`<app>.<server-ip>.sslip.io` with zero DNS setup. Previews are
|
|
19
|
+
independent apps: they never touch the live site, and going live later
|
|
20
|
+
is a plain `plum deploy`.
|
|
21
|
+
- `plum deploy` now preflights DNS on first deploys: Once verifies
|
|
22
|
+
https://<hostname> as its final step and rolls the app back when the
|
|
23
|
+
domain doesn't reach the server, so the CLI checks A records against
|
|
24
|
+
the server's addresses before building anything and asks before
|
|
25
|
+
proceeding (proxied DNS like Cloudflare legitimately mismatches, hence
|
|
26
|
+
a confirm rather than a refusal).
|
|
27
|
+
- Fixed the install generator mounting Plum ABOVE the app's own routes:
|
|
28
|
+
Rails' `route` helper prepends, so an at-"/" mount shadowed everything
|
|
29
|
+
below it — including `get "up"`, the health check that load balancers
|
|
30
|
+
and deploy tooling (Once, kamal-proxy, plum deploy) probe, which made
|
|
31
|
+
freshly scaffolded sites fail health checks with Plum's 404 page. The
|
|
32
|
+
mount is now appended at the bottom of routes.rb with a comment
|
|
33
|
+
explaining why it must stay last. Found by the first real `plum new` →
|
|
34
|
+
`plum deploy` run.
|
|
35
|
+
|
|
36
|
+
- Fixed `plum connect` in a directory with no plum.yml silently targeting
|
|
37
|
+
the globally active project (running it from a fresh repo could show —
|
|
38
|
+
or with --reconfigure, rewrite — a different project's connection).
|
|
39
|
+
Connect now always means "set up the current directory"; only an
|
|
40
|
+
explicit `--project NAME` redirects it.
|
|
41
|
+
- Connect now finishes the journey: when the once app isn't deployed yet
|
|
42
|
+
it offers to build and ship right there (chains into plum deploy), and
|
|
43
|
+
connecting a project always makes it the active one — working in a
|
|
44
|
+
directory means that's the project you're "in". A brand-new site's app
|
|
45
|
+
question also now defaults to "a new app on this server (type its
|
|
46
|
+
hostname)" instead of pre-selecting an already-deployed app, which
|
|
47
|
+
would have silently pointed the repo at another site's container.
|
|
48
|
+
- Untangled project vs. remote in connect's wording: the wizard now asks
|
|
49
|
+
for an "Environment name" (production, staging — plum.yml's remotes)
|
|
50
|
+
with a line explaining the concept, and the closing hint no longer
|
|
51
|
+
prints the nonsensical `plum pull --project production` — it points at
|
|
52
|
+
`plum pull` from the directory, or `--project <site>` from anywhere.
|
|
53
|
+
Terminology now spelled out in docs/plum-cli.md: projects are sites,
|
|
54
|
+
remotes are environments.
|
|
55
|
+
- Connect's server pick-list now knows every stored server, not just the
|
|
56
|
+
plum-* SSH aliases: hosts from every registered project's remotes are
|
|
57
|
+
merged in (annotated "— used by <project>"), so a server appears even
|
|
58
|
+
when its alias offer was declined.
|
|
59
|
+
- Added `plum new NAME`: scaffold a deploy-ready Plum site in one command
|
|
60
|
+
— rails new, the plum-cms gem, the install generator mounted at `/`, a
|
|
61
|
+
prepared database, and a git history. Bootstraps its own toolchain:
|
|
62
|
+
missing Ruby or Rails are offered for install (mise → ruby → rails),
|
|
63
|
+
each step confirm-gated. Refuses clearly when run inside an existing
|
|
64
|
+
Rails app, and registers the new project in the global registry.
|
|
65
|
+
- Added `plum deploy [remote]`: registry-free deploys — build the image
|
|
66
|
+
locally for linux/amd64, stream it to the server over SSH
|
|
67
|
+
(docker save | docker load; no registry, no CI, no accounts), roll the
|
|
68
|
+
Once app to it, and wait for the container health check. Auto-update is
|
|
69
|
+
disabled for these apps: deploys are explicit, never by surprise.
|
|
70
|
+
- Added docs/zero-to-agency.md and a matching tutorial chapter: empty
|
|
71
|
+
directory → client sites on a $6 DigitalOcean droplet with `plum new`,
|
|
72
|
+
`plum connect`, `plum deploy`, and the pull/push/backup rhythm.
|
|
73
|
+
|
|
3
74
|
## 0.2.2 — 2026-08-13
|
|
4
75
|
|
|
5
76
|
### Fixed
|
data/README.md
CHANGED
|
@@ -251,7 +251,9 @@ ergonomics for self-hosted Plum sites:
|
|
|
251
251
|
|
|
252
252
|
```sh
|
|
253
253
|
plum tutorial # interactive guided tour, right in the terminal
|
|
254
|
+
plum new client-site # scaffold a deploy-ready site (installs Rails if needed)
|
|
254
255
|
plum connect 203.0.113.5 # guided setup: SSH keys, server tooling, plum.yml
|
|
256
|
+
plum deploy # build locally, ship over SSH — no registry, no CI
|
|
255
257
|
plum connect # already configured? verifies the setup instead
|
|
256
258
|
plum pull # replace your local site with production's content
|
|
257
259
|
plum push # apply plum/ config-as-code files to the remote
|
|
@@ -265,7 +267,9 @@ A Firebase-style project registry (`plum use`, `--project`) manages a fleet
|
|
|
265
267
|
of sites from one machine. Every command degrades to plain, script-friendly
|
|
266
268
|
output when piped or under `NO_COLOR`. See [the CLI guide](docs/plum-cli.md)
|
|
267
269
|
for transports (`via: ssh|kamal|once`), fleet management, and how `pull`
|
|
268
|
-
works across database engines
|
|
270
|
+
works across database engines — and [the zero-to-agency
|
|
271
|
+
guide](docs/zero-to-agency.md) for running client sites on a single
|
|
272
|
+
cheap VPS with no registry, CI, or PaaS.
|
|
269
273
|
|
|
270
274
|
## Content API
|
|
271
275
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<%= favicon_link_tag "plum-mark.svg", type: "image/svg+xml" %>
|
|
9
9
|
<%= stylesheet_link_tag "plum/control_panel", "data-turbo-track": "reload" %>
|
|
10
10
|
<%= stylesheet_link_tag "lexxy", "data-turbo-track": "reload" %>
|
|
11
|
-
<%= javascript_importmap_tags %>
|
|
11
|
+
<%= javascript_importmap_tags(importmap: Plum.importmap || Rails.application.importmap) %>
|
|
12
12
|
<script type="module">import "plum/application"</script>
|
|
13
13
|
<style>
|
|
14
14
|
:root {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<%= csp_meta_tag %>
|
|
8
8
|
<%= favicon_link_tag "plum-mark.svg", type: "image/svg+xml" %>
|
|
9
9
|
<%= stylesheet_link_tag "plum/control_panel", "data-turbo-track": "reload" %>
|
|
10
|
-
<%= javascript_importmap_tags %>
|
|
10
|
+
<%= javascript_importmap_tags(importmap: Plum.importmap || Rails.application.importmap) %>
|
|
11
11
|
<style>
|
|
12
12
|
:root { --plum-accent: <%= Plum.configuration.cp_accent_color %>; }
|
|
13
13
|
.bg-purple-600 { background-color: var(--plum-accent) !important; }
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<%= favicon_link_tag "plum-mark.svg", type: "image/svg+xml" %>
|
|
9
9
|
<%= stylesheet_link_tag "plum/control_panel", "data-turbo-track": "reload" %>
|
|
10
10
|
<%= stylesheet_link_tag "lexxy", "data-turbo-track": "reload" %>
|
|
11
|
-
<%= javascript_importmap_tags %>
|
|
11
|
+
<%= javascript_importmap_tags(importmap: Plum.importmap || Rails.application.importmap) %>
|
|
12
12
|
<script type="module">import "plum/application"</script>
|
|
13
13
|
<style>
|
|
14
14
|
:root { --plum-accent: <%= Plum.configuration.cp_accent_color %>; }
|
data/docs/plum-cli.md
CHANGED
|
@@ -84,12 +84,78 @@ accepted since the content is public. The host key persists at
|
|
|
84
84
|
`~/.config/plum/tutorial_host_ed25519` so returning visitors don't get
|
|
85
85
|
host-key warnings.
|
|
86
86
|
|
|
87
|
+
## plum new — zero to running site
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
plum new client-site
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Scaffolds a deploy-ready Plum site: `rails new`, the plum-cms gem, the
|
|
94
|
+
install generator mounted at `/`, a prepared database, and a git history.
|
|
95
|
+
It bootstraps its own toolchain the way connect bootstraps a server — no
|
|
96
|
+
Ruby? It offers to install mise, then Ruby, then Rails, each step
|
|
97
|
+
confirm-gated. It also refuses (clearly) to run inside an existing Rails
|
|
98
|
+
app, and registers the new project in the global registry.
|
|
99
|
+
|
|
100
|
+
## plum deploy — no registry, no CI
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
plum deploy [remote]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Ships the site with nothing but local Docker and SSH: builds the image
|
|
107
|
+
locally (`--platform linux/amd64`), streams it to the server through the
|
|
108
|
+
SSH connection (`docker save | gzip | ssh docker load` — no registry
|
|
109
|
+
account anywhere), then has Once roll the app to it (`once update
|
|
110
|
+
--image`, or `once deploy --host <once_app>` on first deploy) and waits
|
|
111
|
+
for the container's health check to go green. Auto-update is switched
|
|
112
|
+
off for these apps: there's no registry to poll, so the site changes
|
|
113
|
+
when you deploy it — never by surprise. Currently `via: once` remotes.
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
plum deploy --preview
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Ships the same image as a **separate Once app under a preview hostname** —
|
|
120
|
+
the agency workflow: share the work-in-progress with a client before
|
|
121
|
+
their domain points anywhere. With `preview_host:` set on the remote in
|
|
122
|
+
plum.yml (e.g. `client.preview.your-agency.com` behind a wildcard A
|
|
123
|
+
record) you get clean branded preview URLs; with nothing configured it
|
|
124
|
+
auto-derives `<app>.<server-ip>.sslip.io` — magic wildcard DNS that
|
|
125
|
+
resolves to your server with zero setup. Preview and production are
|
|
126
|
+
independent apps, so previews never touch a live site.
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
plum promote [remote]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Go-live for a preview, in place: re-homes the preview app onto the
|
|
133
|
+
production domain (`once update --host`) — same container, same
|
|
134
|
+
database, all the content built during the preview — with TLS
|
|
135
|
+
provisioned on the spot. Preflights that a preview exists, that the
|
|
136
|
+
production hostname isn't already a separate app, and that DNS reaches
|
|
137
|
+
the server (confirm to override for proxied DNS).
|
|
138
|
+
|
|
139
|
+
The full story — one droplet, several client sites, honest costs — is
|
|
140
|
+
[the zero-to-agency guide](zero-to-agency.md).
|
|
141
|
+
|
|
87
142
|
## Two layers of configuration
|
|
88
143
|
|
|
89
144
|
This mirrors the Firebase CLI's model deliberately, because it solves the
|
|
90
145
|
same two problems: "how does this directory reach its server" and "how do I
|
|
91
146
|
work with a project without cd-ing into its directory first."
|
|
92
147
|
|
|
148
|
+
Two words, two meanings — keep them straight:
|
|
149
|
+
|
|
150
|
+
- A **project** is a site: `the-final-word`, `client-site`. That's what
|
|
151
|
+
the registry, `plum use`, and `--project NAME` refer to.
|
|
152
|
+
- A **remote** is one *environment* of a site: `production`, `staging`.
|
|
153
|
+
That's the positional argument on commands — `plum pull production`,
|
|
154
|
+
`plum logs staging` — and the keys under `remotes:` in plum.yml.
|
|
155
|
+
|
|
156
|
+
So `plum pull --project client-site staging` means "the staging
|
|
157
|
+
environment of the client-site project."
|
|
158
|
+
|
|
93
159
|
1. **`plum.yml`** — lives in a site's own repo, defines its **remotes**
|
|
94
160
|
(servers). This is the source of truth for how to reach that one site.
|
|
95
161
|
2. **The global project registry** (`~/.config/plum/config.yml`, or
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Zero to Agency
|
|
2
|
+
|
|
3
|
+
How to go from an empty directory to hosting client sites — with one
|
|
4
|
+
cheap VPS, the `plum` CLI, and nothing else. No GitHub required, no
|
|
5
|
+
container registry, no CI pipeline, no platform-as-a-service bill. Your
|
|
6
|
+
laptop builds, your server runs, SSH connects them.
|
|
7
|
+
|
|
8
|
+
The stack under the hood: Rails + Plum in a Docker container, managed by
|
|
9
|
+
[Once](https://once.com) (37signals' single-container deploy tool) behind
|
|
10
|
+
its bundled TLS-terminating proxy, on any Linux box. The examples use
|
|
11
|
+
DigitalOcean; any VPS provider works the same way.
|
|
12
|
+
|
|
13
|
+
## What you need
|
|
14
|
+
|
|
15
|
+
- A [DigitalOcean](https://digitalocean.com) account (or any VPS provider)
|
|
16
|
+
- Docker on your laptop (Docker Desktop or Docker Engine) — `plum deploy`
|
|
17
|
+
builds images locally
|
|
18
|
+
- The `plum` CLI
|
|
19
|
+
- Ruby and Rails — and if you don't have them, `plum new` offers to
|
|
20
|
+
install both for you (via [mise](https://mise.jdx.dev))
|
|
21
|
+
|
|
22
|
+
## 1. Create the server
|
|
23
|
+
|
|
24
|
+
In DigitalOcean: **Create → Droplet**. Ubuntu LTS, the $6/month size is
|
|
25
|
+
enough to start (pick $12 for headroom). Add your SSH key if you have one
|
|
26
|
+
— if you don't, that's fine: `plum connect` will generate one and offer
|
|
27
|
+
to copy it over with the root password DigitalOcean emails you. Note the
|
|
28
|
+
droplet's IP.
|
|
29
|
+
|
|
30
|
+
That's the last time you touch a cloud dashboard.
|
|
31
|
+
|
|
32
|
+
## 2. Create the site
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
plum new client-site
|
|
36
|
+
cd client-site
|
|
37
|
+
bin/rails server
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
You have a running Plum site at `http://localhost:3000` — the control
|
|
41
|
+
panel is at `/cp`. Build the client's theme in `app/themes/`, model
|
|
42
|
+
their content types in the CP (or as YAML in `plum/` — see
|
|
43
|
+
`docs/config-as-code.md`), and load in their content.
|
|
44
|
+
|
|
45
|
+
## 3. Connect the server
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
plum connect 203.0.113.5 # your droplet's IP
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The wizard handles the entire first-contact ritual: SSH keys, passwordless
|
|
52
|
+
login, detecting that this is a Once-shaped app, and — on a fresh droplet —
|
|
53
|
+
**installing Docker Engine and Once for you** (each with your
|
|
54
|
+
confirmation). When it asks for the app hostname, give it the client's
|
|
55
|
+
real domain (`clientname.com`): that's how the server routes requests and
|
|
56
|
+
provisions TLS.
|
|
57
|
+
|
|
58
|
+
## 4. Point DNS
|
|
59
|
+
|
|
60
|
+
At the client's DNS: an **A record** for their domain (and `www`) to the
|
|
61
|
+
droplet's IP. **This must happen before the first deploy** — Once
|
|
62
|
+
verifies `https://<domain>` end-to-end as the final step of a first
|
|
63
|
+
deploy and rolls back if the domain doesn't reach the server yet
|
|
64
|
+
(`plum deploy` checks this up front and warns you). Proxied DNS
|
|
65
|
+
(Cloudflare in Full strict mode) is fine.
|
|
66
|
+
|
|
67
|
+
Building ahead of a domain cutover? That's what previews are for:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
plum deploy --preview
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This ships the site as a *separate* Once app under a preview hostname —
|
|
74
|
+
by default `<app>.<server-ip>.sslip.io`, magic wildcard DNS that reaches
|
|
75
|
+
your server with zero configuration, so you have a shareable client
|
|
76
|
+
preview URL before any real DNS exists. For branded previews, point one
|
|
77
|
+
wildcard record (`*.preview.your-agency.com`) at the droplet and set
|
|
78
|
+
`preview_host: client.preview.your-agency.com` on the remote in
|
|
79
|
+
plum.yml.
|
|
80
|
+
|
|
81
|
+
Iterate on the preview as long as you like — build the content, tune the
|
|
82
|
+
theme, let the client click around. Then, when they flip DNS to the
|
|
83
|
+
droplet:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
plum promote
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The preview app *becomes* the site: same container, same database, every
|
|
90
|
+
word of content you built during the preview — it simply starts answering
|
|
91
|
+
to the real domain, with TLS provisioned on the spot. No rebuild, no
|
|
92
|
+
content migration, no separate go-live deploy. (If the site should live
|
|
93
|
+
on its own droplet instead, `plum connect` it there and `plum deploy` —
|
|
94
|
+
site archives carry the content across.)
|
|
95
|
+
|
|
96
|
+
## 5. Deploy
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
plum deploy
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Watch it go: the image builds on your laptop, streams to the server over
|
|
103
|
+
SSH (`docker save | docker load` — this is why no registry account
|
|
104
|
+
exists in this guide), Once starts it behind the proxy, and the CLI waits
|
|
105
|
+
for the container to report healthy. `https://clientname.com` is live.
|
|
106
|
+
|
|
107
|
+
Deploys are explicit — the site changes when you run `plum deploy`,
|
|
108
|
+
never by surprise. Ship again any time with the same command.
|
|
109
|
+
|
|
110
|
+
## 6. The agency rhythm
|
|
111
|
+
|
|
112
|
+
Day-to-day, the CLI is your relationship with every client site:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
plum pull # production content → your laptop, any DB engine
|
|
116
|
+
plum push # content-model changes (plum/ YAML) → production
|
|
117
|
+
plum check # did anyone drift the model? (CI-friendly)
|
|
118
|
+
plum logs --follow # heroku-style tail, prettified
|
|
119
|
+
plum backup # timestamped archive on the server
|
|
120
|
+
plum run -- TASK # any rake task, any remote
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Content pulls down, structure pushes up. Clients edit in `/cp` on
|
|
124
|
+
production; you develop locally against a full copy of their real
|
|
125
|
+
content. Turn on **static caching** (`docs/static-caching.md`) and the
|
|
126
|
+
site serves rendered pages from disk — SQLite happily runs a
|
|
127
|
+
production site on the smallest droplet, because cached reads never
|
|
128
|
+
touch the database at all.
|
|
129
|
+
|
|
130
|
+
## 7. The second client
|
|
131
|
+
|
|
132
|
+
This is where it becomes an agency:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
plum new second-client
|
|
136
|
+
cd second-client
|
|
137
|
+
plum connect # the host prompt offers your existing server
|
|
138
|
+
plum deploy
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Once runs each site in its own container on the same box, routing by
|
|
142
|
+
hostname, each with its own TLS certificate. A $6–12 droplet comfortably
|
|
143
|
+
hosts several small client sites; each new client is marginal-cost-zero
|
|
144
|
+
until the box runs hot. Manage the fleet from anywhere:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
plum projects list # every site you run
|
|
148
|
+
plum use second-client # switch the active project
|
|
149
|
+
plum pull --project client-site # or target one explicitly
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 8. When you outgrow one box
|
|
153
|
+
|
|
154
|
+
- **More sites than one droplet likes**: create a second droplet,
|
|
155
|
+
`plum connect` the new sites there. The registry doesn't care how many
|
|
156
|
+
servers are behind it.
|
|
157
|
+
- **A client needs Postgres** (heavy dynamic features): switch that app's
|
|
158
|
+
database and redeploy — `plum pull` still works across engines.
|
|
159
|
+
- **Backups off the box**: `plum backup` writes archives on the server;
|
|
160
|
+
Once's auto-backup handles scheduling. Pull copies down with `plum pull`
|
|
161
|
+
or scp them to storage you control.
|
|
162
|
+
|
|
163
|
+
## Costs, honestly
|
|
164
|
+
|
|
165
|
+
| Thing | Cost |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| Droplet (several small sites) | $6–12/mo |
|
|
168
|
+
| Domains | the client's problem |
|
|
169
|
+
| Registry, CI, PaaS, per-seat CMS pricing | $0 — none exist in this setup |
|
|
170
|
+
|
|
171
|
+
The whole model: you charge for design and content strategy; the
|
|
172
|
+
infrastructure is a rounding error.
|
|
@@ -30,7 +30,19 @@ module Plum
|
|
|
30
30
|
def mount_engine
|
|
31
31
|
return if options[:skip_route]
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
# Mounted at "/", Plum serves every path the app doesn't claim — so
|
|
34
|
+
# it must come AFTER the app's own routes (notably `get "up"`, the
|
|
35
|
+
# health check load balancers and deploy tooling probe). Rails'
|
|
36
|
+
# `route` helper prepends, which would shadow them; append at the
|
|
37
|
+
# bottom of the block instead.
|
|
38
|
+
inject_into_file "config/routes.rb", before: /^end\s*\z/ do
|
|
39
|
+
<<~RUBY.indent(2)
|
|
40
|
+
|
|
41
|
+
# Plum last: mounted at #{options[:mount_path].inspect} it serves every path
|
|
42
|
+
# the app doesn't claim, so app routes (like /up) must come first.
|
|
43
|
+
mount Plum::Engine, at: #{options[:mount_path].inspect}
|
|
44
|
+
RUBY
|
|
45
|
+
end
|
|
34
46
|
end
|
|
35
47
|
|
|
36
48
|
def print_next_steps
|
data/lib/plum/engine.rb
CHANGED
|
@@ -30,10 +30,17 @@ module Plum
|
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
# Build a Plum-scoped importmap instead of merging Plum's pins into the
|
|
34
|
+
# host app's global importmap. Plum's layouts render this map (host JS stack
|
|
35
|
+
# + Plum's additions), while the host app's own pages keep a clean importmap
|
|
36
|
+
# free of Plum's controllers/lexxy/activestorage pins. Runs after the host's
|
|
37
|
+
# importmap is configured so we can copy its paths.
|
|
38
|
+
initializer "plum.importmap", after: "importmap" do |app|
|
|
39
|
+
next unless defined?(Importmap::Map) && app.config.respond_to?(:importmap)
|
|
40
|
+
|
|
41
|
+
Plum.importmap = Importmap::Map.new
|
|
42
|
+
Array(app.config.importmap.paths).each { |path| Plum.importmap.draw(path) }
|
|
43
|
+
Plum.importmap.draw(root.join("config/plum_importmap.rb"))
|
|
37
44
|
end
|
|
38
45
|
end
|
|
39
46
|
end
|
data/lib/plum/version.rb
CHANGED
data/lib/plum.rb
CHANGED
|
@@ -3,6 +3,13 @@ require_relative "plum/content_source"
|
|
|
3
3
|
require_relative "plum/configuration"
|
|
4
4
|
|
|
5
5
|
module Plum
|
|
6
|
+
class << self
|
|
7
|
+
# A Plum-scoped importmap (host pins + Plum's own pins), used only by Plum's
|
|
8
|
+
# layouts. Keeps Plum's controllers/lexxy/activestorage out of the host
|
|
9
|
+
# app's global importmap so a mounted Plum can't pollute the host's JS.
|
|
10
|
+
attr_accessor :importmap
|
|
11
|
+
end
|
|
12
|
+
|
|
6
13
|
def self.table_name_prefix
|
|
7
14
|
"plum_"
|
|
8
15
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: plum-cms
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ben Simmons
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bcrypt
|
|
@@ -180,16 +180,22 @@ dependencies:
|
|
|
180
180
|
name: image_processing
|
|
181
181
|
requirement: !ruby/object:Gem::Requirement
|
|
182
182
|
requirements:
|
|
183
|
-
- - "
|
|
183
|
+
- - ">="
|
|
184
184
|
- !ruby/object:Gem::Version
|
|
185
185
|
version: '1.2'
|
|
186
|
+
- - "<"
|
|
187
|
+
- !ruby/object:Gem::Version
|
|
188
|
+
version: '3'
|
|
186
189
|
type: :runtime
|
|
187
190
|
prerelease: false
|
|
188
191
|
version_requirements: !ruby/object:Gem::Requirement
|
|
189
192
|
requirements:
|
|
190
|
-
- - "
|
|
193
|
+
- - ">="
|
|
191
194
|
- !ruby/object:Gem::Version
|
|
192
195
|
version: '1.2'
|
|
196
|
+
- - "<"
|
|
197
|
+
- !ruby/object:Gem::Version
|
|
198
|
+
version: '3'
|
|
193
199
|
description: Plum is a Rails-native CMS engine with Hotwire control panel, Liquid
|
|
194
200
|
themes, and site-scoped content.
|
|
195
201
|
email:
|
|
@@ -404,6 +410,7 @@ files:
|
|
|
404
410
|
- docs/statamic-parity.md
|
|
405
411
|
- docs/static-caching.md
|
|
406
412
|
- docs/vision.md
|
|
413
|
+
- docs/zero-to-agency.md
|
|
407
414
|
- lib/generators/plum/install/install_generator.rb
|
|
408
415
|
- lib/generators/plum/install/templates/plum_initializer.rb
|
|
409
416
|
- lib/plum.rb
|