kirby-link-field
kirby-link-field copied to clipboard
[K4] How to migrate content to native link field
I created a Kirby CLI command for migrating content to the native link field. It's still a work in progress and may not work correctly if you've used more features of the link field than I have. Please run a dry run first and consider this a starting point. Feel free to expand on it and share your improvements.
The command automatically transforms this structure…
Link:
type: url
value: https://example.com
Link2:
type: email
value: [email protected]
…to this:
Link: https://example.com
Link2: mailto:[email protected]
It can also handle JSON content (blocks field) and turns this…
"link": {
"type": "url",
"value": "https://example.com"
}
…to this:
"link": "https://example.com"
Here's the command:
<?php
declare(strict_types=1);
use Kirby\CLI\CLI;
return [
'description' => 'Migrate from Link plugin to native K4 link field',
'args' => [
'verbose' => [
'shortPrefix' => 'v',
'longPrefix' => 'verbose',
'description' => 'Verbose output',
'defaultValue' => false,
'noValue' => true,
],
'dryrun' => [
'longPrefix' => 'dry-run',
'description' => 'Dry run',
'defaultValue' => false,
'noValue' => true,
],
],
'command' => static function (CLI $cli): void {
$directory = kirby()->roots()->content();
$processed = 0;
$replacements = 0;
$isDryRun = $cli->arg('dryrun');
$isVerbose = $cli->arg('verbose');
// Recursively iterate through all files in the content directory
$filenames = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($directory),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($filenames as $filename) {
$filepath = $filename->getPathname();
$relpath = str_replace($directory . '/', '', $filepath);
// Process only .txt files
if ($filename->isDir() || pathinfo($filepath, PATHINFO_EXTENSION) !== 'txt') {
continue;
}
$content = file_get_contents($filepath);
$fileReplacements = 0;
// Process YAML-like structure with JSON content
$content = preg_replace_callback(
'/^(\w+):\s*(\[.*\])$/ms',
function ($matches) use (&$fileReplacements, $isVerbose, $cli, $relpath) {
$fieldName = $matches[1];
$jsonContent = $matches[2];
$json = json_decode($jsonContent, true);
if (json_last_error() === JSON_ERROR_NONE) {
$json = transformNestedJson($json, $fileReplacements);
if ($isVerbose && $fileReplacements > 0) {
$cli->out(" Transformed field '{$fieldName}' in {$relpath}");
}
return $fieldName . ': ' . json_encode($json, JSON_UNESCAPED_SLASHES);
}
return $matches[0];
},
$content
);
// Write changes to file if not a dry run
if (!$isDryRun && $fileReplacements > 0) {
file_put_contents($filepath, $content);
}
// Output processing results
if ($fileReplacements === 0 && $isVerbose) {
$cli->out("⏩ [0] {$relpath}");
} elseif ($fileReplacements > 0) {
$cli->out("✅ [{$fileReplacements}] {$relpath}");
}
$processed++;
$replacements += $fileReplacements;
}
$cli->success("Files processed: {$processed}, Total replacements: {$replacements}");
}
];
/**
* Recursively transform nested JSON structures, including those stored as strings
*
* @param mixed $data The data to transform
* @param int $replacements Reference to the replacement counter
* @return mixed The transformed data
*/
function transformNestedJson($data, &$replacements) {
if (is_array($data)) {
foreach ($data as $key => &$value) {
if (is_array($value)) {
$value = transformNestedJson($value, $replacements);
} elseif (is_string($value) && $key === 'categories') {
// Handle nested JSON stored as a string
$nestedJson = json_decode($value, true);
if (json_last_error() === JSON_ERROR_NONE) {
$value = json_encode(transformNestedJson($nestedJson, $replacements), JSON_UNESCAPED_SLASHES);
}
}
// Check for link structure and transform if found
if (is_array($value) && isset($value['type']) &&
in_array($value['type'], ['url', 'page', 'file', 'email', 'tel'])) {
if (!isset($value['value']) || empty($value['value'])) {
// If there's no value or it's empty, set the entire field to an empty string
$value = '';
} else {
$value = transformLink($value['type'], $value['value']);
}
$replacements++;
}
}
}
return $data;
}
/**
* Transform a single link based on its type
*
* @param string $type The type of the link (url, page, file, email, tel)
* @param string $value The value of the link
* @return string The transformed link value
*/
function transformLink(string $type, string $value): string {
if (empty($value)) {
return '';
}
switch ($type) {
case 'email':
return 'mailto:' . $value;
case 'tel':
return 'tel:' . $value;
default:
return $value;
}
}