summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-07-14 19:24:26 +0200
committererdgeist <erdgeist@erdgeist.org>2026-07-14 19:24:26 +0200
commit8c602ca1fb749127d4acb4cb8b471f5a0332c3e2 (patch)
tree330a79fc1897762e2d1689df5f586014788d8495
parent6ddc9d73f1299cdeb90bc88ed0bb53821c4825aa (diff)
Add scaffolding for a proper tech documentation from everything I could remember
-rw-r--r--CONTRIBUTING.md89
1 files changed, 89 insertions, 0 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..c26956c
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,89 @@
1# Contributing to CCCMS
2
3## Stack
4Rails 8.1, Ruby 3.2, PostgreSQL 16, ImageMagick 7 (`magick`, not `convert`).
5No Sprockets pipeline for public or admin static assets except
6`admin_bundle.js` — everything else is served straight from `public/` and
7cache-busted via file mtime (`ApplicationHelper#mtime_busted_path`), not
8fingerprinted content hashing.
9
10## Content model
11- **Node** — the tree structure (`awesome_nested_set`-derived, migrating
12 toward a plain `parent_id` model — both may be present during that
13 transition). Holds three separate Page references: `head` (published),
14 `draft` (being worked on), `autosave` (in-progress typing).
15- **Page** — one revision's content. `belongs_to :node`, except `autosave`
16 layers specifically: their `node_id` is deliberately `nil`, which is what
17 keeps them out of `node.pages` (the real revision history) entirely —
18 an autosave is not a revision.
19- **Page::Translation** — Globalize's own generated class (`translates
20 :title, :abstract, :body` on `Page`). Not a top-level `PageTranslation`
21 constant, and it has `page_id`, not a `page` association reader.
22
23## Publishing lifecycle
24There is exactly one path from an edit to a published change:
25`autosave!` (writes into the autosave layer, creating it via
26`clone_attributes_from` on the previous head/draft if it doesn't exist yet)
27→ `save_draft!` (clones the *entire* autosave into the draft layer,
28wholesale — every locale, not just the one just edited) → `publish_draft!`
29(promotes draft to head). Nothing writes to `draft` or `head` directly.
30Locking (`lock_for_editing!`, raising `LockedByAnotherUser`) gates every
31step; a lock can exist with no draft or autosave at all (acquisition is
32deliberately separate from content creation).
33
34## Locale architecture
35Presentation locale (which language the admin chrome or public page
36renders in) and content-target locale (which translation a given screen is
37reading or writing) are two different concerns and never share a
38mechanism. The route's `:locale` segment governs the former only. The
39primary node editor always targets the default locale, enforced via
40`Globalize.with_locale` (never `I18n.locale`, which would leak into
41generated URLs through `default_url_options`). Non-default-locale editing
42uses a separately-named route param (`:translation_locale`), deliberately
43distinct from `:locale` so the two can never be conflated by a link helper.
44Globalize's configured fallback chain is correct for public rendering and
45wrong for editing — anywhere a screen needs to know a translation's real,
46unborrowed state, read the `Page::Translation` row directly rather than the
47locale-dependent attribute accessor.
48
49## Assets
50`FileAttachment` (`app/models/concerns/file_attachment.rb`) is a from-
51scratch Paperclip replacement. `STYLES` is a hash of style name to a full
52ImageMagick argument array (not just a geometry string — some styles need
53`-gravity`/`-extent` alongside `-resize`, which a single string can't
54express). `generate_variants` loops over `STYLES` generically; adding a
55style needs no other code change, only `bundle exec rake
56assets:regenerate_variants` to backfill existing assets. The URL contract
57(`/system/uploads/:id/:style/:filename`) is stable and safe to parse
58directly (the numeric id is always the second path segment) rather than
59reconstructed through model methods.
60
61## Aggregator / shortcode system
62`[aggregate ...]` in page body text is parsed by
63`ContentHelper#aggregate?` into an options hash, then executed by
64`Page.aggregate`. Only `tags`, `children` (`"direct"`/`"all"`, combined
65with the current node), `order_by`/`order_direction`, and `limit` are
66actually consumed. Only the first `[aggregate ...]` in a given body is
67ever processed (`aggregate?` does a single, non-global match).
68
69## TinyMCE integration
70`extended_valid_elements` must define a parent element and its commonly-
71nested children symmetrically and explicitly together — leaving one
72element on the schema's default rule while the other is explicitly
73redefined can cause content loss on save, even when the child would be
74perfectly valid under the untouched default alone. `valid_classes` denies
75all classes by default (`'*': ''`); anything meant to render needs an
76explicit per-element allowance. `content_style` injects CSS into the
77editing iframe directly — the app's real public stylesheet is never loaded
78there, so anything that needs to render correctly while editing needs its
79own rule supplied this way. Constrained-input features (fixed placement
80choices, for instance) are built as custom toolbar buttons with their own
81dialog and `editor.insertContent()`, not the native image dialog or
82`file_picker_callback` — those bring their own resize/edit affordances that
83would undermine a genuinely fixed set of choices.
84
85## Testing
86Shared test-only convenience methods (that need to operate across many
87model/controller test files) live directly on `ActiveSupport::TestCase`,
88in `test/test_helper.rb` — the framework's own designated extension point
89for exactly this, not a monkey-patch of a production model class.