-
Notifications
You must be signed in to change notification settings - Fork 0
feat: [Content Hub] PageTypes: NewsHolder, ArticlePage, PressReleasePage, CaseStudyPage, TagPage #234
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: [Content Hub] PageTypes: NewsHolder, ArticlePage, PressReleasePage, CaseStudyPage, TagPage #234
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,13 @@ | ||||||
| --- | ||||||
| Name: contenthub | ||||||
| --- | ||||||
| SilverStripe\Core\Injector\Injector: | ||||||
| SilverStripe\Control\Director: | ||||||
| properties: | ||||||
| rules: | ||||||
| 'news//$Action/$ID/$Name': | ||||||
| Controller: App\ContentHub\NewsHolderController | ||||||
|
||||||
| Controller: App\ContentHub\NewsHolderController | |
| Controller: App\ContentHub\NewsHolder |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,151 @@ | ||||||||||||||||||||||||||||||||||||
| <?php | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| namespace App\ContentHub; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| use Page; | ||||||||||||||||||||||||||||||||||||
| use PageController; | ||||||||||||||||||||||||||||||||||||
| use SilverStripe\Assets\Image; | ||||||||||||||||||||||||||||||||||||
| use SilverStripe\Forms\CheckboxField; | ||||||||||||||||||||||||||||||||||||
| use SilverStripe\Forms\DateField; | ||||||||||||||||||||||||||||||||||||
| use SilverStripe\Forms\FieldList; | ||||||||||||||||||||||||||||||||||||
| use SilverStripe\Forms\HTMLEditor\HTMLEditorField; | ||||||||||||||||||||||||||||||||||||
| use SilverStripe\Forms\TextField; | ||||||||||||||||||||||||||||||||||||
| use SilverStripe\Forms\TextareaField; | ||||||||||||||||||||||||||||||||||||
| use SilverStripe\Forms\TreeMultiselectField; | ||||||||||||||||||||||||||||||||||||
| use SilverStripe\ORM\DataList; | ||||||||||||||||||||||||||||||||||||
| use SilverStripe\ORM\DB; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||
| * ArticlePage | ||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||
| * Standard news/blog article with date, author, tags. | ||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||
| class ArticlePage extends Page | ||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||
| private static $table_name = 'SiteTree_ArticlePage'; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| private static $singular_name = 'Article'; | ||||||||||||||||||||||||||||||||||||
| private static $plural_name = 'Articles'; | ||||||||||||||||||||||||||||||||||||
| private static $description = 'A news or blog article with metadata, author info, and tags'; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| private static $db = [ | ||||||||||||||||||||||||||||||||||||
| 'Date' => 'Date', | ||||||||||||||||||||||||||||||||||||
| 'Subtitle' => 'Varchar(255)', | ||||||||||||||||||||||||||||||||||||
| 'AuthorName' => 'Varchar(255)', | ||||||||||||||||||||||||||||||||||||
| 'AuthorRole' => 'Varchar(100)', | ||||||||||||||||||||||||||||||||||||
| 'AuthorBio' => 'Text', | ||||||||||||||||||||||||||||||||||||
| 'IsFeatured' => 'Boolean', | ||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| private static $has_one = [ | ||||||||||||||||||||||||||||||||||||
| 'FeaturedImage' => Image::class, | ||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+40
to
+42
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| private static $many_many = [ | ||||||||||||||||||||||||||||||||||||
| 'Tags' => Tag::class, | ||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| private static $casting = [ | ||||||||||||||||||||||||||||||||||||
| 'Excerpt' => 'Text', | ||||||||||||||||||||||||||||||||||||
| 'Date' => 'Text', | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
| 'Date' => 'Text', |
Copilot
AI
Apr 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TreeMultiselectField is a tree UI intended for hierarchical data; Tag is a plain DataObject. This is likely to break tag selection in the CMS (and doesn’t allow creating tags). Consider using a relation-appropriate field (CheckboxSetField/GridField/tagfield) for the many_many Tags relation.
Copilot
AI
Apr 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RelatedArticles() hardcodes the many_many join table name (ArticlePage_Tags) and builds a manual SQL join/where. This is brittle (naming can differ with namespacing/config) and bypasses ORM conveniences. Prefer expressing this via ORM relation filters (e.g., filtering on Tags.ID) and using distinct()/deduping via ORM to avoid the manual join.
| return ArticlePage::get() | |
| ->filter(['ID:not' => $this->ID]) | |
| ->innerJoin( | |
| 'ArticlePage_Tags', | |
| '"ArticlePage_Tags"."ArticlePageID" = "SiteTree"."ID"' | |
| ) | |
| ->where(sprintf( | |
| '"ArticlePage_Tags"."TagID" IN (%s)', | |
| implode(',', array_map('intval', $tagIds)) | |
| )) | |
| ->sort('"Date" DESC') | |
| ->groupBy('"SiteTree"."ID"') | |
| return static::get() | |
| ->exclude('ID', $this->ID) | |
| ->filter(['Tags.ID' => $tagIds]) | |
| ->distinct() | |
| ->sort('"Date" DESC') |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,99 @@ | ||||||||
| <?php | ||||||||
|
|
||||||||
| namespace App\ContentHub; | ||||||||
|
|
||||||||
| use Page; | ||||||||
| use PageController; | ||||||||
| use SilverStripe\Assets\Image; | ||||||||
| use SilverStripe\Forms\FieldList; | ||||||||
| use SilverStripe\Forms\GridField\GridField; | ||||||||
| use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor; | ||||||||
| use SilverStripe\Forms\TextField; | ||||||||
| use SilverStripe\Forms\TextareaField; | ||||||||
| use SilverStripe\Forms\TreeMultiselectField; | ||||||||
| use SilverStripe\ORM\DataList; | ||||||||
|
|
||||||||
| /** | ||||||||
| * CaseStudyPage | ||||||||
| * | ||||||||
| * Customer success story with problem/solution/results structure. | ||||||||
| */ | ||||||||
| class CaseStudyPage extends Page | ||||||||
| { | ||||||||
| private static $table_name = 'SiteTree_CaseStudyPage'; | ||||||||
|
|
||||||||
| private static $singular_name = 'Case Study'; | ||||||||
| private static $plural_name = 'Case Studies'; | ||||||||
| private static $description = 'A customer success story with Problem → Solution → Results structure'; | ||||||||
|
|
||||||||
| private static $db = [ | ||||||||
| 'ClientName' => 'Varchar(255)', | ||||||||
| 'ClientIndustry'=> 'Varchar(100)', | ||||||||
| 'ClientWebsite' => 'Varchar(255)', | ||||||||
| 'Challenge' => 'HTMLText', // The problem | ||||||||
| 'Solution' => 'HTMLText', // What we did | ||||||||
| 'Results' => 'HTMLText', // Outcomes | ||||||||
| 'Quote' => 'Text', // Client testimonial quote | ||||||||
| 'QuoteAuthor' => 'Varchar(255)', // Who said the quote | ||||||||
| ]; | ||||||||
|
|
||||||||
| private static $has_one = [ | ||||||||
| 'CaseStudyImage' => Image::class, | ||||||||
| 'Logo' => Image::class, // Client logo | ||||||||
| ]; | ||||||||
|
|
||||||||
| private static $many_many = [ | ||||||||
| 'Results' => 'App\ContentHub\CaseStudyMetric', | ||||||||
|
||||||||
| 'Results' => 'App\ContentHub\CaseStudyMetric', | |
| 'Metrics' => 'App\ContentHub\CaseStudyMetric', |
Copilot
AI
Apr 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getCMSFields() adds a Tags field, but this PageType doesn’t declare a many_many Tags relation. This will trigger a “relation does not exist” error when building the field. Add the missing many_many definition (consistent with ArticlePage/PressReleasePage) or remove the field.
Copilot
AI
Apr 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CaseStudyMetric is referenced as a relation target, but there is no CaseStudyMetric class in the repository (searching for class CaseStudyMetric returns no matches). This will cause a fatal error when the ORM builds the relation. Add the missing DataObject class (and any required config), or remove/replace this relation.
| private static $many_many = [ | |
| 'Results' => 'App\ContentHub\CaseStudyMetric', | |
| ]; |
Copilot
AI
Apr 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TreeMultiselectField is a tree UI intended for hierarchical data; Tag is a plain DataObject. Even once the Tags relation exists, this field type is unlikely to work correctly for selecting tags. Consider switching to a relation-appropriate selector (CheckboxSetField/GridField/tagfield) so tags can be managed reliably in the CMS.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,90 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <?php | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| namespace App\ContentHub; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use Page; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use SilverStripe\Assets\Image; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use SilverStripe\Core\Injector\Injector; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use SilverStripe\Dev\Deprecation; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use SilverStripe\Forms\DateField; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use SilverStripe\Forms\FieldList; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use SilverStripe\Forms\GridField\GridField; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use SilverStripe\Forms\GridField\GridFieldConfig_RecordPaginator; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use SilverStripe\Forms\HTMLEditor\HTMLEditorField; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use SilverStripe\Forms\TextField; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use SilverStripe\Forms\TreeMultiselectField; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use SilverStripe\ORM\ArrayList; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use SilverStripe\ORM\DataList; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use PageController; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * NewsHolder | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Parent PageType that holds Articles, Press Releases, and Case Studies. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Provides unified feed, per-type listing methods, and CMS management. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class NewsHolder extends Page | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static $table_name = 'NewsHolder'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static $singular_name = 'News Hub'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static $plural_name = 'News Hubs'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static $description = 'Parent page holding Articles, Press Releases, and Case Studies'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static $allowed_children = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ArticlePage::class, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PressReleasePage::class, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CaseStudyPage::class, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public function getCMSFields(): FieldList | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $fields = parent::getCMSFields(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $config = GridFieldConfig_RecordPaginator::create(20); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $fields->addFieldToTab('Root.Articles', GridField::create( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Articles', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Articles', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ArticlePage::get()->filter(['ParentID' => $this->ID]), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $config | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+43
to
+50
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $fields->addFieldToTab('Root.PressReleases', GridField::create( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'PressReleases', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Press Releases', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PressReleasePage::get()->filter(['ParentID' => $this->ID]), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $config | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+43
to
+58
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $config = GridFieldConfig_RecordPaginator::create(20); | |
| $fields->addFieldToTab('Root.Articles', GridField::create( | |
| 'Articles', | |
| 'Articles', | |
| ArticlePage::get()->filter(['ParentID' => $this->ID]), | |
| $config | |
| )); | |
| $fields->addFieldToTab('Root.PressReleases', GridField::create( | |
| 'PressReleases', | |
| 'Press Releases', | |
| PressReleasePage::get()->filter(['ParentID' => $this->ID]), | |
| $config | |
| )); | |
| $articleConfig = GridFieldConfig_RecordPaginator::create(20); | |
| $pressReleaseConfig = GridFieldConfig_RecordPaginator::create(20); | |
| $caseStudyConfig = GridFieldConfig_RecordPaginator::create(20); | |
| $fields->addFieldToTab('Root.Articles', GridField::create( | |
| 'Articles', | |
| 'Articles', | |
| ArticlePage::get()->filter(['ParentID' => $this->ID]), | |
| $articleConfig | |
| )); | |
| $fields->addFieldToTab('Root.PressReleases', GridField::create( | |
| 'PressReleases', | |
| 'Press Releases', | |
| PressReleasePage::get()->filter(['ParentID' => $this->ID]), | |
| $pressReleaseConfig | |
| )); | |
| $fields->addFieldToTab('Root.CaseStudies', GridField::create( | |
| 'CaseStudies', | |
| 'Case Studies', | |
| CaseStudyPage::get()->filter(['ParentID' => $this->ID]), | |
| $caseStudyConfig | |
| )); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Routing rules are configured under
SilverStripe\Core\Injector\Injectorhere, but elsewhere in the codebase routing is declared directly underSilverStripe\Control\Director: rules:(e.g.,app/_config/routes.ymlandapp/_config/about_routes.yml). This config block likely won’t be picked up as a route definition. Move the rule underSilverStripe\Control\Director:at the top level to match existing routing config.