plutonium 0.24.1 → 0.24.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb793ad2d24a4aef3c7a5c46146260b019e6ab1af1e82a23c110379f96b51df6
4
- data.tar.gz: 3926d88a01fa0a9ec45b2c072b0c8112e058082520fb4d2019f765a8cf2a97a3
3
+ metadata.gz: 26384f4aed52055be1bcc886fcb5df8c98d84297254ab95c8a5de40c6263f492
4
+ data.tar.gz: 870630d6315cc1e2964a6fea922e276f52957fcf7a0a6d381dc52d6e84a96c1e
5
5
  SHA512:
6
- metadata.gz: 4bf56145b9fc26fe50d167bfdffd347488c4be2d7a32c985d698ca4c24e40fc98cfc26e3a3c5cf88fca7dbf8de934f4c4974e8d394b347918c1f1d962484ace6
7
- data.tar.gz: dacc135a569a2c5ec92134e7cf2670948680622c529514e2ddb996318e4e120330081ea8fd122b6c4dd9e29e3e6c838dcb037ca0939f65e16a43c73f2f50851e
6
+ metadata.gz: 649df44bd7ad78590c769afffacbbd604919c246f3e1db98eb0397509d7111b24bc9eec2a13bd311c59ac2d1a42d1b9cef06ce7a8e15a6ae69012066ce645a67
7
+ data.tar.gz: 1ac6bc80a0788d79c6d99211134a45afce8ed54d6dfff156766c44587314951a561733de01c7c3a8e6abc41d06c0235542a7f4085ca4041fb599459497f18e0b
@@ -1,6 +1,6 @@
1
1
  <%= render Plutonium::UI::Layout::Header.new do |header| %>
2
2
  <% header.with_brand_logo do %>
3
- <%= resource_logo_tag(classname: "h-10") %>
3
+ <%= resource_logo_tag(classname: "h-10 rounded-md") %>
4
4
  <% end %>
5
5
 
6
6
  <% header.with_action do %>
@@ -56,6 +56,7 @@ export default defineConfig(withMermaid({
56
56
  items: [
57
57
  { text: "Resources", link: "/guide/deep-dive/resources" },
58
58
  { text: "Authorization", link: "/guide/deep-dive/authorization" },
59
+ { text: "Multitenancy", link: "/guide/deep-dive/multitenancy" },
59
60
  { text: "Modules", link: "/modules/" },
60
61
  ]
61
62
  },
@@ -0,0 +1,256 @@
1
+ # Deep Dive: Multitenancy
2
+
3
+ Plutonium is designed to make building sophisticated, multi-tenant applications straightforward. Multitenancy allows you to serve distinct groups of users (like different companies or teams) from a single application instance, ensuring each group's data is kept private and isolated.
4
+
5
+ This is achieved through a powerful feature called **Entity Scoping**, which automates data isolation, URL generation, and authorization with minimal setup.
6
+
7
+ ::: tip What You'll Learn
8
+ - The core concept of Entity Scoping
9
+ - A step-by-step guide to configuring multitenancy
10
+ - The practical benefits of automatic data isolation and routing
11
+ - Advanced patterns for complex multi-tenant scenarios
12
+ - Best practices for security, testing, and performance
13
+ :::
14
+
15
+ ## How Entity Scoping Works
16
+
17
+ Entity Scoping is the heart of Plutonium's multitenancy. Instead of manually filtering data in every controller and model, you declare a **Scoping Entity** for a user-facing **Portal**. This entity is typically a model like `Organization`, `Account`, or `Workspace`.
18
+
19
+ Once configured, Plutonium handles the rest automatically.
20
+
21
+ ::: code-group
22
+ ```ruby [Without Plutonium Scoping]
23
+ # Manual filtering is required everywhere
24
+ class PostsController < ApplicationController
25
+ def index
26
+ # Manually find the organization and scope posts
27
+ @organization = Organization.find(params[:organization_id])
28
+ @posts = @organization.posts
29
+ end
30
+
31
+ def create
32
+ # Manually associate new records with the organization
33
+ @organization = Organization.find(params[:organization_id])
34
+ @post = @organization.posts.build(post_params)
35
+ # ...
36
+ end
37
+ end
38
+ ```
39
+
40
+ ```ruby [With Plutonium Scoping]
41
+ # Scoping is automatic and declarative
42
+ class PostsController < ResourceController
43
+ def index
44
+ # current_authorized_scope automatically returns posts scoped to the current organization
45
+ # When using path strategy: URL -> /organizations/123/posts
46
+ # When using subdomain strategy: URL -> /posts (on acme.yourapp.com)
47
+ end
48
+
49
+ def create
50
+ # resource_params automatically includes the current organization association
51
+ # Scoping works regardless of the strategy used
52
+ end
53
+ end
54
+ ```
55
+ :::
56
+
57
+ ## Setting Up Your Multi-Tenant Portal
58
+
59
+ Configuring multitenancy involves three main steps: defining your strategy, implementing it, and ensuring your models are correctly associated.
60
+
61
+ ### 1. Configure Your Portal Engine
62
+
63
+ First, you tell your portal which entity to scope to and which strategy to use. This is done in the portal's `engine.rb` file using `scope_to_entity`.
64
+
65
+ A **Scoping Strategy** tells Plutonium how to identify the current tenant for each request.
66
+
67
+ ::: code-group
68
+ ```ruby [Path Strategy (Most Common)]
69
+ # packages/admin_portal/lib/engine.rb
70
+ # Tenant is identified by a URL parameter, e.g., /organizations/:organization_id
71
+ scope_to_entity Organization, strategy: :path
72
+ ```
73
+
74
+ ```ruby [Custom Strategy (Subdomain)]
75
+ # packages/customer_portal/lib/engine.rb
76
+ # Tenant is identified by the a custom strategy that checks the subdomain, e.g., acme.yourapp.com
77
+ scope_to_entity Organization, strategy: :current_organization
78
+ ```
79
+
80
+ ```ruby [Custom Parameter Key]
81
+ # packages/client_portal/lib/engine.rb
82
+ # Same as :path, but with a custom parameter name.
83
+ scope_to_entity Client,
84
+ strategy: :path,
85
+ param_key: :client_slug # URL -> /clients/:client_slug
86
+ ```
87
+ :::
88
+
89
+ You can also use any custom name for your strategy method (the method name becomes the strategy).
90
+
91
+ ### 2. Implement the Strategy Method
92
+
93
+ If you use any strategy other than `:path`, you must implement a controller method that returns the current tenant object. The method name must exactly match the strategy name.
94
+
95
+ This logic typically lives in your portal's base controller concern.
96
+
97
+ ::: code-group
98
+ ```ruby [Subdomain Strategy]
99
+ # packages/customer_portal/app/controllers/customer_portal/concerns/controller.rb
100
+ private
101
+
102
+ # Method name :current_organization matches the strategy
103
+ def current_organization
104
+ @current_organization ||= Organization.find_by!(subdomain: request.subdomain)
105
+ rescue ActiveRecord::RecordNotFound
106
+ redirect_to root_path, error: "Invalid organization subdomain"
107
+ end
108
+ ```
109
+
110
+ ```ruby [Session-Based Strategy]
111
+ # packages/internal_portal/app/controllers/internal_portal/concerns/controller.rb
112
+ # In engine.rb: scope_to_entity Workspace, strategy: :current_workspace
113
+ private
114
+
115
+ def current_workspace
116
+ return @current_workspace if defined?(@current_workspace)
117
+
118
+ workspace_id = session[:workspace_id] || params[:workspace_id]
119
+ @current_workspace = current_user.workspaces.find(workspace_id)
120
+
121
+ session[:workspace_id] = @current_workspace.id # Remember for next request
122
+ @current_workspace
123
+ rescue ActiveRecord::RecordNotFound
124
+ redirect_to workspace_selection_path, error: "Please select a workspace"
125
+ end
126
+ ```
127
+ :::
128
+
129
+ ### 3. Connect Your Models
130
+
131
+ Plutonium needs to understand how your resources relate to the scoping entity. It automatically discovers these relationships in three ways:
132
+
133
+ ::: code-group
134
+ ```ruby [1. Direct Association (Preferred)]
135
+ # The model belongs directly to the scoping entity.
136
+ class Post < ApplicationRecord
137
+ belongs_to :organization # Direct link
138
+ end
139
+ ```
140
+
141
+ ```ruby [2. Indirect Association]
142
+ # The model belongs to another model that has the direct link.
143
+ # Plutonium automatically follows the chain: Comment -> Post -> Organization
144
+ class Comment < ApplicationRecord
145
+ belongs_to :post
146
+ has_one :organization, through: :post # Indirect link
147
+ end
148
+ ```
149
+
150
+ ```ruby [3. Custom Scope (For Complex Cases)]
151
+ # For complex relationships, you can define an explicit scope.
152
+ # The scope name must be `associated_with_#{scoping_entity_name}`.
153
+ class Invoice < ApplicationRecord
154
+ belongs_to :customer
155
+
156
+ scope :associated_with_organization, ->(organization) do
157
+ joins(customer: :organization_memberships)
158
+ .where(organization_memberships: { organization_id: organization.id })
159
+ end
160
+ end
161
+ ```
162
+ :::
163
+
164
+ ## The Benefits in Practice
165
+
166
+ With this setup complete, you gain several powerful features across your portal.
167
+
168
+ ### Tenant-Aware Routing
169
+
170
+ Your application's URLs are automatically transformed to include the tenant context, and Plutonium's URL helpers adapt accordingly.
171
+
172
+ - **URL Transformation:** Routes like `/posts` and `/posts/123` become `/:organization_id/posts` and `/:organization_id/posts/123`.
173
+ - **Automatic URL Generation:** The `resource_url_for` helper automatically includes the current tenant in all generated URLs, so links and forms work without any changes.
174
+
175
+ ```ruby
176
+ # Both of these helpers are automatically aware of the current tenant.
177
+ resource_url_for(Post) # => "/organizations/456/posts"
178
+ form_with model: @post # action -> "/organizations/456/posts/123"
179
+ ```
180
+
181
+ ### Secure Data Scoping
182
+
183
+ All data access is automatically and securely filtered to the current tenant.
184
+
185
+ - **Query Scoping:** A query like `Post.all` is automatically converted to `current_scoped_entity.posts`. This prevents accidental data leaks.
186
+ - **Record Scoping:** When fetching a single record (e.g., for `show` or `edit`), Plutonium ensures it belongs to the current tenant. If not, it raises an `ActiveRecord::RecordNotFound` error, just as if the record didn't exist.
187
+
188
+ ### Integrated Authorization
189
+
190
+ The current `entity_scope` is seamlessly passed to your authorization policies, allowing for fine-grained, tenant-aware rules.
191
+
192
+ ```ruby
193
+ class PostPolicy < Plutonium::Resource::Policy
194
+ # `entity_scope` is automatically available in all policy methods.
195
+ authorize :entity_scope, allow_nil: true
196
+
197
+ def update?
198
+ # A user can only update a post if it belongs to their current tenant
199
+ # AND they are the author of the post.
200
+ record.organization == entity_scope && record.author == user
201
+ end
202
+
203
+ relation_scope do |relation|
204
+ # `super` automatically applies the base entity scoping.
205
+ relation = super(relation)
206
+
207
+ # Add more logic: Admins can see all posts within their organization,
208
+ # but others can only see published posts.
209
+ user.admin? ? relation : relation.where(published: true)
210
+ end
211
+ end
212
+ ```
213
+
214
+ ## Security Best Practices
215
+
216
+ Securing a multi-tenant application is critical. While Plutonium provides strong defaults, you must ensure your implementation is secure.
217
+
218
+ ::: warning Always Validate Tenant Access
219
+ A user might belong to multiple tenants. It's crucial to verify that the logged-in user has permission to access the tenant specified in the URL. Failure to do so could allow a user to see data from another organization they don't belong to.
220
+ :::
221
+
222
+ ```ruby
223
+ # ✅ Good: Proper tenant validation
224
+ # In your custom strategy method or a before_action:
225
+ private
226
+
227
+ def current_organization
228
+ @current_organization ||= begin
229
+ # Find the organization from the URL
230
+ organization = Organization.find(params[:organization_id])
231
+
232
+ # CRITICAL: Verify the current user is a member of that organization
233
+ unless current_user.organizations.include?(organization)
234
+ raise ActionPolicy::Unauthorized, "Access denied to organization"
235
+ end
236
+
237
+ organization
238
+ end
239
+ end
240
+
241
+ # ❌ Dangerous: No access validation
242
+ def current_organization
243
+ # This allows ANY authenticated user to access ANY organization's data
244
+ # simply by changing the ID in the URL.
245
+ Organization.find(params[:organization_id])
246
+ end
247
+ ```
248
+
249
+ ## Advanced Patterns
250
+
251
+ Plutonium's scoping is flexible enough to handle more complex scenarios.
252
+
253
+ - **Multi-Level Tenancy:** For hierarchical tenancy (e.g., Company -> Department), you can apply the primary scope at the engine level and add secondary scoping logic inside your policies' `relation_scope`.
254
+ - **Cross-Tenant Data Access:** For resources that can be shared, define a custom `associated_with_...` scope that includes both shared records and records belonging to the current tenant.
255
+ - **Tenant Switching:** Build a controller that allows users to change their active tenant by updating a `session` key, then use a session-based scoping strategy to read it.
256
+ - **API Multitenancy:** Create a custom scoping strategy (e.g., `:api_tenant`) that authenticates and identifies the tenant based on an API key or JWT from the request headers.
@@ -188,7 +188,7 @@ Portal Controllers provide specialized functionality for multi-tenant applicatio
188
188
 
189
189
  ```ruby
190
190
  module AdminPortal
191
- class PostsController < PlutoniumController
191
+ class PostsController < ResourceController
192
192
  include AdminPortal::Concerns::Controller
193
193
 
194
194
  # Automatically inherits from ::PostsController
data/docs/modules/core.md CHANGED
@@ -76,7 +76,7 @@ The Bootable concern automatically detects which package and engine a controller
76
76
 
77
77
  ```ruby
78
78
  # In packages/admin_portal/app/controllers/users_controller.rb
79
- class UsersController < PlutoniumController
79
+ class UsersController < ResourceController
80
80
  # Automatically detects:
81
81
  # current_package => AdminPortal
82
82
  # current_engine => AdminPortal::Engine
@@ -221,13 +221,13 @@ The Core module is typically used through other Plutonium modules:
221
221
 
222
222
  ```ruby
223
223
  # For resource controllers
224
- class UsersController < PlutoniumController
224
+ class UsersController < ResourceController
225
225
  include Plutonium::Resource::Controller
226
226
  # Core module included automatically
227
227
  end
228
228
 
229
229
  # For portal controllers
230
- class DashboardController < PlutoniumController
230
+ class DashboardController < ResourceController
231
231
  include Plutonium::Portal::Controller
232
232
  # Core module included automatically
233
233
  end
@@ -294,12 +294,12 @@ Let the Bootable concern handle package detection automatically:
294
294
 
295
295
  ```ruby
296
296
  # ✅ Good - automatic detection
297
- class AdminPortal::UsersController < PlutoniumController
297
+ class AdminPortal::UsersController < ResourceController
298
298
  # Package and engine automatically detected
299
299
  end
300
300
 
301
301
  # ❌ Avoid - manual configuration
302
- class UsersController < PlutoniumController
302
+ class UsersController < ResourceController
303
303
  def current_package
304
304
  AdminPortal # Don't override unless necessary
305
305
  end
@@ -61,7 +61,7 @@ module Plutonium
61
61
  identifier: title.parameterize,
62
62
  title: -> { h5(class: "text-2xl font-bold tracking-tight text-gray-900 dark:text-white") { title } }
63
63
  ) do
64
- FrameNavigatorPanel(title: "", src:)
64
+ FrameNavigatorPanel(title: "", src:, panel_id: "association-panel-#{title.parameterize}")
65
65
  end
66
66
  end
67
67
 
@@ -15,8 +15,34 @@ module Plutonium
15
15
  private
16
16
 
17
17
  def form_action
18
- # interactive action forms post to the same page
19
- nil
18
+ # Build the correct commit URL for the interactive action
19
+ action = helpers.current_interactive_action
20
+ return nil unless action
21
+
22
+ # Create route options for the commit action (convert GET to POST action)
23
+ commit_route_options = action.route_options.merge(
24
+ Plutonium::Action::RouteOptions.new(
25
+ method: :post,
26
+ action: commit_action_name(action.route_options.url_options[:action])
27
+ )
28
+ )
29
+
30
+ # Use existing infrastructure to build the URL
31
+ subject = action.record_action? ? helpers.resource_record! : helpers.resource_class
32
+ helpers.route_options_to_url(commit_route_options, subject)
33
+ end
34
+
35
+ def commit_action_name(action_name)
36
+ case action_name
37
+ when :interactive_record_action
38
+ :commit_interactive_record_action
39
+ when :interactive_resource_action
40
+ :commit_interactive_resource_action
41
+ when :interactive_collection_action
42
+ :commit_interactive_bulk_action
43
+ else
44
+ action_name
45
+ end
20
46
  end
21
47
 
22
48
  def initialize_attributes
@@ -41,20 +41,22 @@ module Plutonium
41
41
  end
42
42
 
43
43
  class PanelContent < Plutonium::UI::Component::Base
44
- def initialize(src:)
44
+ def initialize(id:, src:)
45
+ @id = id
45
46
  @src = src
46
47
  end
47
48
 
48
49
  def view_template
49
- DynaFrameHost src: @src, loading: :lazy, data: {"frame-navigator-target": "frame"} do
50
+ DynaFrameHost id: @id, src: @src, loading: :lazy, data: {"frame-navigator-target": "frame"} do
50
51
  SkeletonTable()
51
52
  end
52
53
  end
53
54
  end
54
55
 
55
- def initialize(title:, src:)
56
+ def initialize(title:, src:, panel_id: nil)
56
57
  @title = title
57
58
  @src = src
59
+ @panel_id = panel_id
58
60
  end
59
61
 
60
62
  def view_template
@@ -65,7 +67,7 @@ module Plutonium
65
67
  panel.with_item PanelItem.new(label: "Back", icon: Phlex::TablerIcons::ChevronLeft, data_frame_navigator_target: "backButton")
66
68
  panel.with_item PanelItem.new(label: "Refresh", icon: Phlex::TablerIcons::RefreshDot, data_frame_navigator_target: "refreshButton")
67
69
  panel.with_item PanelLink.new(label: "Maximize", icon: Phlex::TablerIcons::WindowMaximize, href: @src, data_frame_navigator_target: "maximizeLink")
68
- panel.with_content PanelContent.new(src: @src)
70
+ panel.with_content PanelContent.new(id: @panel_id, src: @src)
69
71
  end
70
72
  end
71
73
  end
@@ -11,7 +11,7 @@ module Plutonium
11
11
  end
12
12
 
13
13
  def main_attributes = mix(super, {
14
- class: "flex flex-col items-center justify-center px-6 py-8 mx-auto lg:py-0"
14
+ class: "flex flex-col items-center justify-center gap-2 px-6 py-8 mx-auto lg:py-0"
15
15
  })
16
16
 
17
17
  def render_content(&)
@@ -25,8 +25,8 @@ module Plutonium
25
25
  end
26
26
 
27
27
  def render_logo
28
- link_to root_path, class: "flex items-center text-2xl font-semibold text-gray-900 dark:text-white" do
29
- helpers.resource_logo_tag classname: "w-24 h-24 mr-2"
28
+ link_to root_path, class: "flex items-center text-2xl font-semibold text-gray-900 dark:text-white mb-2" do
29
+ helpers.resource_logo_tag classname: "w-24 h-24 mr-2 rounded-md"
30
30
  end
31
31
  end
32
32
 
@@ -1,5 +1,5 @@
1
1
  module Plutonium
2
- VERSION = "0.24.1"
2
+ VERSION = "0.24.3"
3
3
  NEXT_MAJOR_VERSION = VERSION.split(".").tap { |v|
4
4
  v[1] = v[1].to_i + 1
5
5
  v[2] = 0
@@ -0,0 +1,45 @@
1
+ # So, You've Decided to Build Another SaaS...
2
+
3
+ Let me guess. You have a brilliant idea for a new web application. It's going to be the next big thing. You're fired up, ready to code. You spin up a new Rails app, and then it hits you. The boilerplate. User authentication, admin panels, data tables, filters, authorization policies... Suddenly your brilliant idea is buried under a mountain of tedious, repetitive work.
4
+
5
+ We've all been there. Rails is great, but for complex applications, the initial setup can be a drag. You end up writing the same code over and over again, just to get to the starting line.
6
+
7
+ What if you could skip all that? What if you could get straight to building the features that make your app unique?
8
+
9
+ Enter Plutonium.
10
+
11
+ ## What's This Plutonium Thing, Anyway?
12
+
13
+ Plutonium is a Rapid Application Development (RAD) toolkit for Rails. That's a fancy way of saying it helps you build feature-rich, enterprise-ready applications at a ridiculous speed. It's not a replacement for Rails; it's a high-level abstraction built on top of it. You still get all the Rails goodness you know and love, but with a whole lot of awesome packed on top.
14
+
15
+ The core idea behind Plutonium is its **Resource-Oriented Architecture**. In a Plutonium app, everything is a "Resource". A user is a Resource, a product is a Resource, a blog post is a Resource. You get the idea.
16
+
17
+ But these aren't just your plain old `ActiveRecord` models. A Plutonium Resource is a supercharged, self-contained unit that knows how to do a bunch of things on its own. Each Resource is made up of four parts:
18
+
19
+ * **The Model:** This is your good old `ActiveRecord` model. Nothing new here. It still handles your database interactions, validations, and associations.
20
+ * **The Definition:** This is where things get interesting. The Definition tells Plutonium how the Resource should look and behave in the UI. You define fields, search functionality, filters, and custom actions, all in a clean, declarative way.
21
+ * **The Policy:** This is for authorization. It controls who can do what with a Resource. It's like having a bouncer for every piece of data in your app.
22
+ * **The Actions:** These are for custom operations. Think of anything that's not a simple create, read, update, or delete. For example, an action to "publish" a blog post or "deactivate" a user.
23
+
24
+ This structure keeps your code incredibly organized and easy to reason about.
25
+
26
+ ## Stop Organizing, Start Building
27
+
28
+ One of the biggest headaches in a large Rails app is keeping the code organized. As the app grows, `app/models`, `app/controllers`, and `app/views` can become a real mess. Plutonium solves this with a modular packaging system.
29
+
30
+ There are two types of packages:
31
+
32
+ * **Feature Packages:** These are where your core business logic lives. They're self-contained modules focused on a specific domain, like "invoicing" or "user management". They don't have any web-facing parts, just the raw logic.
33
+ * **Portal Packages:** These are the user interfaces. A portal takes one or more feature packages and presents them to a specific type of user, like an "admin portal" or a "customer portal".
34
+
35
+ This separation is a game-changer. It forces you to think about your application in a more structured way, which pays off big time in the long run.
36
+
37
+ And if you're building a multi-tenant SaaS app, you'll love this: Plutonium has built-in support for multi-tenancy. You can scope all your data to an entity like an `Organization` or `Account` with a single line of code in your portal configuration. Plutonium handles the rest, automatically ensuring that users only see the data that belongs to them.
38
+
39
+ To top it all off, Plutonium comes with a bunch of generators that create all the boilerplate for you. Need a new feature package? `rails generate pu:pkg:package my_feature`. Need a new resource with a model, definition, policy, and all the fixings? `rails generate pu:res:scaffold post title:string content:text`. It's like having a junior developer who does all the boring work for you.
40
+
41
+ ## Is Plutonium for You?
42
+
43
+ Plutonium is not for every project. If you're building a simple blog or a marketing site, it's probably overkill. But if you're building a complex business application, a multi-tenant SaaS platform, an admin system, or a CMS, Plutonium can be a massive productivity boost.
44
+
45
+ So, next time you have that brilliant idea for a new app, maybe give Plutonium a try. You might just find that you can get from idea to launch a whole lot faster.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plutonium
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.24.1
4
+ version: 0.24.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
@@ -495,6 +495,7 @@ files:
495
495
  - docs/api-examples.md
496
496
  - docs/guide/cursor-rules.md
497
497
  - docs/guide/deep-dive/authorization.md
498
+ - docs/guide/deep-dive/multitenancy.md
498
499
  - docs/guide/deep-dive/resources.md
499
500
  - docs/guide/getting-started/01-installation.md
500
501
  - docs/guide/index.md
@@ -881,6 +882,7 @@ files:
881
882
  - lib/tasks/.keep
882
883
  - package-lock.json
883
884
  - package.json
885
+ - plutonium-blog-post.md
884
886
  - postcss-gem-import.js
885
887
  - postcss.config.js
886
888
  - public/.keep