From 332de16246195ed96429b49801ce40f9bf691b41 Mon Sep 17 00:00:00 2001 From: indy koning Date: Fri, 24 Apr 2026 16:19:43 +0200 Subject: [PATCH] Added migration code and pipelines to migrate Magento pages to Statamic --- composer.json | 1 + config/rapidez/statamic/migration.php | 32 +++++ src/Actions/CleanHtml.php | 23 +++ src/Actions/ConvertField.php | 73 ++++++++++ src/Commands/MigrateCmsPages.php | 134 ++++++++++++++++++ src/Pipelines/BardData/FixListItems.php | 57 ++++++++ src/Pipelines/Html/HtmlThroughMarkdown.php | 32 +++++ src/Pipelines/Html/TransformIframes.php | 27 ++++ .../Html/TransformRapidezContent.php | 14 ++ src/Pipelines/Markdown/TransformImages.php | 39 +++++ src/RapidezStatamicServiceProvider.php | 6 +- 11 files changed, 437 insertions(+), 1 deletion(-) create mode 100644 config/rapidez/statamic/migration.php create mode 100644 src/Actions/CleanHtml.php create mode 100644 src/Actions/ConvertField.php create mode 100644 src/Commands/MigrateCmsPages.php create mode 100644 src/Pipelines/BardData/FixListItems.php create mode 100644 src/Pipelines/Html/HtmlThroughMarkdown.php create mode 100644 src/Pipelines/Html/TransformIframes.php create mode 100644 src/Pipelines/Html/TransformRapidezContent.php create mode 100644 src/Pipelines/Markdown/TransformImages.php diff --git a/composer.json b/composer.json index 1504e15..9376d8e 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,7 @@ "require": { "php": "^8.4", "justbetter/statamic-glide-directive": "dev-feature/statamic-6", + "league/html-to-markdown": "^5.1", "rapidez/blade-directives": "^1.0", "rapidez/core": "^5.0", "rapidez/sitemap": "^5.0", diff --git a/config/rapidez/statamic/migration.php b/config/rapidez/statamic/migration.php new file mode 100644 index 0000000..5e869a5 --- /dev/null +++ b/config/rapidez/statamic/migration.php @@ -0,0 +1,32 @@ + [ + 'clean_html' => [ + /** + * These pipelines are executed by the CleanHtml action **in this order** + */ + 'html' => [ + TransformRapidezContent::class, + TransformIframes::class, + HtmlThroughMarkdown::class, + ], + 'markdown' => [ + TransformImages::class, + ] + ], + 'convert_field' => [ + FixListItems::class + ] + ] +]; \ No newline at end of file diff --git a/src/Actions/CleanHtml.php b/src/Actions/CleanHtml.php new file mode 100644 index 0000000..1289018 --- /dev/null +++ b/src/Actions/CleanHtml.php @@ -0,0 +1,23 @@ +pipeline + ->send($html) + ->via('handle') + ->through(config('rapidez.statamic.migration.pipelines.clean_html.html', [])) + ->thenReturn(); + } +} diff --git a/src/Actions/ConvertField.php b/src/Actions/ConvertField.php new file mode 100644 index 0000000..b93cac7 --- /dev/null +++ b/src/Actions/ConvertField.php @@ -0,0 +1,73 @@ +blueprint()->field($fieldName)?->fieldType(); + if ($fieldType === null) { + throw new InvalidArgumentException(__('Entry with blueprint ":blueprint" does not contain field ":fieldname"', ['blueprint' => $entry->blueprint()->handle, 'fieldname' => $fieldName])); + } + + if (!$fieldType instanceof Replicator || (!$fieldType instanceof Bard && !$fieldType->flattenedSetsConfig()->hasAny('content'))) { + throw new InvalidArgumentException(__('":fieldname" on Blueprint ":blueprint" is not a Bard field or is not a replicator containing a Bard field', ['blueprint' => $entry->blueprint()->handle, 'fieldname' => $fieldName])); + } + + $isReplicator = false; + if (!($fieldType instanceof Bard) && $fieldType instanceof Replicator) { + $isReplicator = true; + } + + $html = $this->cleanHtml->execute($html); + if (trim($html) === '') { + return $entry; + } + + $augmentor = new Augmentor($fieldType instanceof Bard ? $fieldType : resolve(Bard::class)); + $data = $augmentor->renderHtmlToProsemirror($html); + $content = $this->pipeline + ->send($data['content']) + ->via('handle') + ->through(config('rapidez.statamic.migration.pipelines.convert_field', [])) + ->thenReturn(); + + $currentContent = $isReplicator ? data_get($entry->get($fieldName), '0.content.content') : $entry->get($fieldName); + if ( + $currentContent + // We must check by rendering to html since the array structure changes, but the html output does not. + && $augmentor->renderProsemirrorToHtml(['content' => $currentContent]) == $augmentor->renderProsemirrorToHtml(['content' => $content]) + ) { + return $entry; + } + + if ($isReplicator) { + // TODO: Currently we only support a fieldset named "content", containing a field called "content" which is a Bard field. + // Ideally we should determine the needed path by nesting fields in the Replicator until we find a Bard field. + $content = [[ + 'type' => 'content', + 'content' => ['content' => $content] + ]]; + } + + $entry->set($fieldName, $content); + + return $entry; + } +} diff --git a/src/Commands/MigrateCmsPages.php b/src/Commands/MigrateCmsPages.php new file mode 100644 index 0000000..2ec8189 --- /dev/null +++ b/src/Commands/MigrateCmsPages.php @@ -0,0 +1,134 @@ + $sitePerStoreId */ + private Collection $sitePerStoreId; + + /** + * Execute the console command. + */ + public function handle(ConvertField $converter): void + { + $this->converter = $converter; + + $this->sitePerStoreId = Site::all() + ->filter(fn(\Statamic\Sites\Site $site) => $site->attribute('magento_store_id')) + ->mapWithKeys(fn(\Statamic\Sites\Site $site, $key): array => [$site->attribute('magento_store_id') => $site]); + + foreach ($this->sitePerStoreId as $storeId => $site) { + Rapidez::setStore($storeId); + + $this->createCmsPages($site); + } + + $this->output->writeln(__('Don\'t forget to disable the pages in Magento or remove the cms page fallback route! ":line"', ['line' => 'Rapidez::removeFallbackRoute(CmsPageController::class);'])); + } + + public function createCmsPages(\Statamic\Sites\Site $site): void + { + /** @var Page $cmsPageModel */ + $cmsPageModel = config('rapidez.models.page'); + $pagesQuery = $cmsPageModel::query(); + + if ($this->option('identifiers')) { + $identifiers = is_array($this->option('identifiers')) ? $this->option('identifiers') : explode(',', $this->option('identifiers')); + $pagesQuery->when($this->option('identifier-type') === 'blacklist', fn($q) => $q->whereNotIn('identifier', $identifiers)); + $pagesQuery->when($this->option('identifier-type') !== 'blacklist', fn($q) => $q->whereIn('identifier', $identifiers)); + } + + $this->output->progressStart($pagesQuery->count()); + + foreach ($pagesQuery->lazy() as $page) { + $this->output->progressAdvance(); + + /** @var EntryQueryBuilder $query */ + $query = Entry::query(); + if ($query + ->where('collection', 'pages') + ->where('slug', $page->identifier) + ->where('site', $site->handle) + ->exists() + ) { + continue; + } + + $this->createCmsPage($page, $site); + } + + $this->output->progressFinish(); + } + + private function createCmsPage($page, \Statamic\Sites\Site $site): void + { + if (trim((string) $page->content) === '') { + return; + } + + $pageStores = DB::table('cms_page_store')->where('page_id', $page->page_id)->pluck('store_id'); + + /** @var EntryQueryBuilder $query */ + $query = Entry::query(); + $query + ->where('collection', 'pages') + ->where('slug', $page->identifier); + if ($page->store_id !== 0) { + $query->whereIn('site', $this->sitePerStoreId->filter(fn(\Statamic\Sites\Site $aSite): bool => in_array($aSite->attribute('magento_store_id'), $pageStores->toArray()))->map(fn(\Statamic\Sites\Site $aSite) => $aSite->handle())); + } + + /** @var ?\Statamic\Entries\Entry $originEntry */ + $originEntry = $query->first(); + + if ($originEntry !== null) { + /** @var \Statamic\Entries\Entry $entry */ + $entry = $originEntry->makeLocalization($site->handle); + } else { + /** @var \Statamic\Entries\Entry $entry */ + $entry = Entry::make(); + $entry + ->collection('pages') + ->locale($site) + ->published(1) + ->slug($page->identifier); + } + + $entry->title = Rapidez::content($page->title); // @phpstan-ignore property.notFound + + $this->converter->execute($entry, $page->content, 'page_builder'); + + $entry->seo = [ // @phpstan-ignore property.notFound + 'title' => Rapidez::content($page->meta_title), + 'description' => Rapidez::content($page->meta_description), + ]; + + $entry->save(); + } +} diff --git a/src/Pipelines/BardData/FixListItems.php b/src/Pipelines/BardData/FixListItems.php new file mode 100644 index 0000000..12456d7 --- /dev/null +++ b/src/Pipelines/BardData/FixListItems.php @@ -0,0 +1,57 @@ +fixListItems($data)); + } + + /** + * Recursively walk any value (array, nested arrays) and fix listItem nodes + * whose content consists entirely of inline nodes (e.g. bare "text" nodes) + * by wrapping them in a paragraph node, as Statamic's Bard editor requires. + */ + public function fixListItems(mixed $data): mixed + { + if (!is_array($data)) { + return $data; + } + + if (($data['type'] ?? null) === 'listItem' && isset($data['content'])) { + $data['content'] = $this->wrapInlineContentInParagraph($data['content']); + } + + foreach ($data as $key => $value) { + $data[$key] = $this->fixListItems($value); + } + + return $data; + } + + /** + * @param array $content + * @return array + */ + private function wrapInlineContentInParagraph(array $content): array + { + $allInline = collect($content)->every( + fn (mixed $node): bool => !in_array($node['type'] ?? null, self::BLOCK_NODE_TYPES, true) + ); + + if (!$allInline) { + return $content; + } + + return [['type' => 'paragraph', 'content' => $content]]; + } +} diff --git a/src/Pipelines/Html/HtmlThroughMarkdown.php b/src/Pipelines/Html/HtmlThroughMarkdown.php new file mode 100644 index 0000000..b02bde4 --- /dev/null +++ b/src/Pipelines/Html/HtmlThroughMarkdown.php @@ -0,0 +1,32 @@ +converter = new HtmlConverter(['strip_tags' => true]); + $this->converter->getEnvironment()->addConverter(new TableConverter()); + } + + public function handle(string $html, Closure $next): string + { + $markdown = $this->converter->convert($html); + $markdown = $this->pipeline + ->send($markdown) + ->via('handle') + ->through(config('rapidez.statamic.migration.pipelines.clean_html.markdown', [])) + ->thenReturn(); + + return $next(Str::markdown($markdown)); + } +} diff --git a/src/Pipelines/Html/TransformIframes.php b/src/Pipelines/Html/TransformIframes.php new file mode 100644 index 0000000..999c08d --- /dev/null +++ b/src/Pipelines/Html/TransformIframes.php @@ -0,0 +1,27 @@ +]*src="(https?://[^"]+)"[^>]*>(?:)?#', $this->transformIframe(...), $html)); + } + + /** + * @param array $matches + */ + private function transformIframe(array $matches): string + { + $value = (string) $matches[1]; + + if (str_contains($value, 'youtube.com/embed/')) { + $value = str_replace('youtube.com/embed/', 'youtube.com/watch?v=', $value); + } + + return ''.$value.''; + } +} diff --git a/src/Pipelines/Html/TransformRapidezContent.php b/src/Pipelines/Html/TransformRapidezContent.php new file mode 100644 index 0000000..a8971ba --- /dev/null +++ b/src/Pipelines/Html/TransformRapidezContent.php @@ -0,0 +1,14 @@ +transformImage(...), $markdown)); + } + + /** + * @param array $matches + */ + private function transformImage(array $matches): string + { + $value = (string) $matches[1]; + if (!str_contains($value, '/directive/___directive/')) { + return $value; + } + + $parts = explode('/', $value); + $key = array_search("___directive", $parts, true); + if ($key !== false) { + $value = $parts[$key+1]; + $value = base64_decode(strtr($value, '-_,', '+/=')); + + $parts = explode('"', $value); + $key = array_search("{{media url=", $parts, true); + $value = $parts[$key+1]; + $value = Str::finish(config('rapidez.media_url'), '/') . ltrim($value, '/'); + } + + return $value; + } +} diff --git a/src/RapidezStatamicServiceProvider.php b/src/RapidezStatamicServiceProvider.php index ed2b0fd..91bea0f 100644 --- a/src/RapidezStatamicServiceProvider.php +++ b/src/RapidezStatamicServiceProvider.php @@ -17,6 +17,7 @@ use Rapidez\Statamic\Commands\InvalidateCacheCommand; use Rapidez\Statamic\Contracts\ImportsBrands; use Rapidez\Statamic\Actions\ImportBrands as ImportBrandsAction; +use Rapidez\Statamic\Commands\MigrateCmsPages; use Rapidez\Statamic\Extend\SitesLinkedToMagentoStores; use Rapidez\Statamic\Forms\JsDrivers\Vue; use Rapidez\Statamic\Http\ViewComposers\ConfigComposer; @@ -114,7 +115,8 @@ public function bootCommands() : self $this->commands([ ImportBrands::class, InstallCommand::class, - InvalidateCacheCommand::class + InvalidateCacheCommand::class, + MigrateCmsPages::class ]); return $this; @@ -124,6 +126,7 @@ public function bootConfig() : self { $this->mergeConfigFrom(__DIR__.'/../config/rapidez/statamic.php', 'rapidez.statamic'); $this->mergeConfigFrom(__DIR__ . '/../config/rapidez/statamic/builder.php', 'rapidez.statamic.builder'); + $this->mergeConfigFrom(__DIR__ . '/../config/rapidez/statamic/migration.php', 'rapidez.statamic.migration'); return $this; } @@ -242,6 +245,7 @@ public function bootPublishables() : self $this->publishes([ __DIR__.'/../config/rapidez/statamic.php' => config_path('rapidez/statamic.php'), __DIR__ . '/../config/rapidez/statamic/builder.php' => config_path('rapidez/statamic/builder.php'), + __DIR__ . '/../config/rapidez/statamic/migration.php' => config_path('rapidez/statamic/migration.php'), ], 'config'); $this->publishes([