diff --git a/.docker/Dockerfile b/.docker/Dockerfile
new file mode 100644
index 0000000000..ccb97e4fe5
--- /dev/null
+++ b/.docker/Dockerfile
@@ -0,0 +1,18 @@
+FROM php:8.5-cli
+
+ARG DEBIAN_FRONTEND=noninteractive
+
+# 2. Install system dependencies and Node.js/npm
+# We are using Node.js 22 LTS as a reliable, modern standard
+RUN apt-get update && apt-get install -y \
+ curl gnupg \
+ && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
+ && apt-get install -y nodejs \
+ && apt-get clean \
+ && rm -rf /var/lib/lists/*
+
+# 3. Globally install TypeScript and the Socket Dev CLI
+RUN npm install -g typescript socket tsx esbuild
+
+# 4. Set up a default working directory (Optional)
+WORKDIR /app
diff --git a/.router.php b/.router.php
index c204ce635e..98ce5a1773 100644
--- a/.router.php
+++ b/.router.php
@@ -4,12 +4,19 @@
$filename = $_SERVER["PATH_INFO"] ?? $_SERVER["SCRIPT_NAME"];
+//die(print_r($_SERVER, true));
+
+//$_SERVER['HTTP_HOST'] = '';
+//$_SERVER['BASE_PAGE'] = '/';
+//$_SERVER['SERVER_NAME'] = 'localhost';
+
if (!file_exists($_SERVER["DOCUMENT_ROOT"] . $filename)) {
require_once __DIR__ . '/error.php';
return;
}
+
/* This could be an image or whatever, so don't try to compress it */
ini_set("zlib.output_compression", 0);
return false;
diff --git a/bin/createNewsEntry b/bin/createNewsEntry
index 937330218a..f4d8868558 100755
--- a/bin/createNewsEntry
+++ b/bin/createNewsEntry
@@ -180,6 +180,8 @@ function parseOptions(): Entry {
echo " --conf-time 'value' When the event will be occurign (cfp and conference categories only)\n";
echo " --content 'value' Text content for the entry, may include XHTML\n";
echo " --content-file 'value' Name of file to load content from, may not be specified with --content\n";
+ echo " --summary 'value' Short, plain-text summary of the content. Should be a single line.\n";
+ echo " --summary-file 'value' Name of the file to load summary content from, should be a plain-text summary of the content. Should be a single line.\n";
echo " --image-path 'value' Basename of image file in " . Entry::IMAGE_PATH_REL . "\n";
echo " --image-title 'value' Title for the image provided\n";
echo " --image-link 'value' URI to direct to when clicking the image\n";
@@ -238,5 +240,22 @@ function parseOptions(): Entry {
exit(1);
}
+ $summary = $opts['summary'] ?? '';
+ $summaryFile = $opts['summary-file'] ?? '';
+ if ($summary && $summaryFile) {
+ fwrite(STDERR, "--summary and --summary-file may not be specified together\n");
+ exit(1);
+ } elseif ($summaryFile) {
+ $summary = file_get_contents($summaryFile);
+ if ($summary === false) {
+ fwrite(STDERR, "Summary cannot be opened, or must not be empty\n");
+ exit(1);
+ }
+ }
+
+ if ($summary) {
+ $entry->setSummary($summary);
+ }
+
return $entry;
}
diff --git a/docs.php b/docs.php
index a7003afd07..c531c7a159 100644
--- a/docs.php
+++ b/docs.php
@@ -7,54 +7,46 @@
site_header("Documentation", ["current" => "docs"]);
+$lang = Languages::ACTIVE_ONLINE_LANGUAGES_EX['en'];
?>
Documentation
-
-
-
- The PHP Manual is available online in a selection of languages.
- Please pick a language from the list below.
-
-
-
- Note, that many languages are just under translation, and
- the untranslated parts are still in English. Also some translated
- parts might be outdated. The translation teams are
- open to contributions.
-
+
PHP Translates its documentation into many different languages, but the main reference is in English
+ Note, that many languages are just under translation, and
+ the untranslated parts are still in English. Also some translated
+ parts might be outdated. The translation teams are
+ open to contributions.
+
+
+ $lang) { ?>
+
+
+
+
= htmlspecialchars($lang['label_loc']) ?>
+
= htmlspecialchars($lang['label_en']) ?>
+
+
+
+
+
-?>
-
+
-
- The language currently being used as the default for you should be in
- bold above. You can change the setting for this on the
- My PHP.net customization page.
-
-
+
More Options
For downloadable formats, please visit our
documentation downloads page.
diff --git a/images/landing/contribute.png b/images/landing/contribute.png
new file mode 100644
index 0000000000..07d570ab2c
Binary files /dev/null and b/images/landing/contribute.png differ
diff --git a/images/landing/docs-translator.png b/images/landing/docs-translator.png
new file mode 100644
index 0000000000..1f28200e8f
Binary files /dev/null and b/images/landing/docs-translator.png differ
diff --git a/images/landing/externals-io.svg b/images/landing/externals-io.svg
new file mode 100644
index 0000000000..b1480073ef
--- /dev/null
+++ b/images/landing/externals-io.svg
@@ -0,0 +1,4 @@
+
diff --git a/images/landing/php-bugs.png b/images/landing/php-bugs.png
new file mode 100644
index 0000000000..e3f3004315
Binary files /dev/null and b/images/landing/php-bugs.png differ
diff --git a/images/language-flags/br.png b/images/language-flags/br.png
new file mode 100644
index 0000000000..eb88391c1a
Binary files /dev/null and b/images/language-flags/br.png differ
diff --git a/images/language-flags/de.png b/images/language-flags/de.png
new file mode 100644
index 0000000000..0a62147326
Binary files /dev/null and b/images/language-flags/de.png differ
diff --git a/images/language-flags/en.webp b/images/language-flags/en.webp
new file mode 100644
index 0000000000..833789753e
Binary files /dev/null and b/images/language-flags/en.webp differ
diff --git a/images/language-flags/es.png b/images/language-flags/es.png
new file mode 100644
index 0000000000..ce714320f5
Binary files /dev/null and b/images/language-flags/es.png differ
diff --git a/images/language-flags/fr.png b/images/language-flags/fr.png
new file mode 100644
index 0000000000..94a8acbec1
Binary files /dev/null and b/images/language-flags/fr.png differ
diff --git a/images/language-flags/it.png b/images/language-flags/it.png
new file mode 100644
index 0000000000..66568e44fa
Binary files /dev/null and b/images/language-flags/it.png differ
diff --git a/images/language-flags/ja.png b/images/language-flags/ja.png
new file mode 100644
index 0000000000..f009df369b
Binary files /dev/null and b/images/language-flags/ja.png differ
diff --git a/images/language-flags/ru.png b/images/language-flags/ru.png
new file mode 100644
index 0000000000..fdd45a9b75
Binary files /dev/null and b/images/language-flags/ru.png differ
diff --git a/images/language-flags/tr.png b/images/language-flags/tr.png
new file mode 100644
index 0000000000..3510020b54
Binary files /dev/null and b/images/language-flags/tr.png differ
diff --git a/images/language-flags/uk.webp b/images/language-flags/uk.webp
new file mode 100644
index 0000000000..16abefbd4e
Binary files /dev/null and b/images/language-flags/uk.webp differ
diff --git a/images/language-flags/zh.webp b/images/language-flags/zh.webp
new file mode 100644
index 0000000000..fb0a0d1d30
Binary files /dev/null and b/images/language-flags/zh.webp differ
diff --git a/images/logos/composer.png b/images/logos/composer.png
new file mode 100644
index 0000000000..e0782d0593
Binary files /dev/null and b/images/logos/composer.png differ
diff --git a/images/logos/github_invertocat_white.svg b/images/logos/github_invertocat_white.svg
new file mode 100644
index 0000000000..527ba11c50
--- /dev/null
+++ b/images/logos/github_invertocat_white.svg
@@ -0,0 +1,10 @@
+
diff --git a/images/logos/php-foundation.svg b/images/logos/php-foundation.svg
new file mode 100644
index 0000000000..cf5b3a6942
--- /dev/null
+++ b/images/logos/php-foundation.svg
@@ -0,0 +1,11 @@
+
diff --git a/images/logos/phpc-discord.png b/images/logos/phpc-discord.png
new file mode 100644
index 0000000000..7a79320cc7
Binary files /dev/null and b/images/logos/phpc-discord.png differ
diff --git a/images/logos/reddit.png b/images/logos/reddit.png
new file mode 100644
index 0000000000..9ce7ce2541
Binary files /dev/null and b/images/logos/reddit.png differ
diff --git a/include/branch-meta.inc b/include/branch-meta.inc
new file mode 100644
index 0000000000..c68e1eae25
--- /dev/null
+++ b/include/branch-meta.inc
@@ -0,0 +1,92 @@
+ [
+ 'support_label' => 'Supported',
+ 'features' => [
+ [
+ 'title' => 'URI Extension',
+ 'about' => 'PHP 8.5 adds a built-in URI extension to parse, normalize, and handle URLs following RFC 3986 and WHATWG URL standards.',
+ ],
+ [
+ 'title' => 'Pipe Operator',
+ 'about' => 'The |> operator enables chaining callables left-to-right, passing values smoothly through multiple functions without intermediary variables.',
+ ],
+ [
+ 'title' => 'Clone With',
+ 'about' => 'Clone objects and update properties with the new clone() syntax, making the "with-er" pattern simple for readonly classes.',
+ ],
+ [
+ 'title' => '#[\NoDiscard] Attribute',
+ 'about' => 'The #[\NoDiscard] attribute warns when a return value isn’t used, helping prevent mistakes and improving overall API safety.',
+ ],
+ [
+ 'title' => 'Closures and First-Class Callables in Constant Expressions',
+ 'about' => 'Static closures and first-class callables can now be used in constant expressions, such as attribute parameters.',
+ ],
+ [
+ 'title' => 'Persistent cURL Share Handles',
+ 'about' => 'Handles can now be persisted across multiple PHP requests, avoiding the cost of repeated connection initialization to the same hosts.',
+ ],
+ ],
+ ],
+ '8.4' => [
+ 'support_label' => 'Supported',
+ 'features' => [
+ [
+ 'title' => 'Property Hooks',
+ 'short' => 'Property Hooks allow intercepting properties',
+ ],
+ [
+ 'title' => 'Asymmetric Property Visibility',
+ 'short' => 'Asymmetric Visibility for get and set',
+ ],
+ [
+ 'title' => '#[Derpeciated] Attribute',
+ 'short' => '#[Depreciated] attribute signals removal intent',
+ ],
+ [
+ 'title' => 'Additional Array Functions',
+ 'short' => 'New array lookup and query options',
+ ],
+ ],
+ ],
+ '8.3' => [
+ 'support_label' => 'Security Support',
+ 'features' => [
+ [
+ 'title' => 'Typed Class Constants',
+ 'short' => 'Class constants can now be typed',
+ ],
+ [
+ 'title' => 'Dynamic Class Constants',
+ 'about' => 'Class constants can now be accessed via dynamic calls',
+ ],
+ [
+ 'title' => 'Readonly Deep Cloning',
+ 'short' => 'Enhanced deep cloning of readonly instances',
+ ],
+ [
+ 'title' => 'Randomizer Improvements',
+ 'short' => 'Generate random strings from provided character sets',
+ ],
+ ],
+ ],
+ '8.2' => [
+ 'support_label' => 'Security Support',
+ 'features' => [
+ [
+ 'title' => 'Readonly classes',
+ 'short' => 'Entire classes can now be marked Readonly',
+ ],
+ [
+ 'title' => 'Disjunction Normal Form Types',
+ 'short' => 'Improved type support with Disjunction Normal Forms',
+ ],
+ [
+ 'title' => 'Improved Standalone Types',
+ 'short' => 'Null, true and false are now usable as types',
+ ],
+ ],
+ ],
+];
diff --git a/include/branches.inc b/include/branches.inc
index deb1f79841..bf56899bc4 100644
--- a/include/branches.inc
+++ b/include/branches.inc
@@ -89,6 +89,11 @@ function format_interval($from, DateTime $to) {
return $eolPeriod;
}
+function get_distribution_base_url(): string
+{
+ return 'https://www.php.net/distributions';
+}
+
function version_number_to_branch(string $version): ?string {
$parts = explode('.', $version);
if (count($parts) > 1) {
@@ -138,9 +143,14 @@ function get_all_branches() {
function get_active_branches($include_recent_eols = true) {
$branches = [];
$now = new DateTime();
+ $baseUrl = get_distribution_base_url();
foreach ($GLOBALS['RELEASES'] as $major => $releases) {
foreach ($releases as $version => $release) {
+ foreach ($release['source'] as $sourceId => $source) {
+ $releases[$version]['source'][$sourceId]['url'] = $baseUrl . $source['filename'];
+ }
+
$branch = version_number_to_branch($version);
if ($branch) {
diff --git a/include/communities.inc b/include/communities.inc
new file mode 100644
index 0000000000..5b1dc7d5bd
--- /dev/null
+++ b/include/communities.inc
@@ -0,0 +1,22 @@
+ 'Reddit',
+ 'about' => 'Reddit has an active PHP community discussing the language and its ecosystem.',
+ 'image' => '/images/logos/reddit.png',
+ 'href' => 'https://www.reddit.com/r/PHP/',
+ ],
+ [
+ 'title' => 'PHP Community Discord',
+ 'about' => 'Join thousands of users on Discord talking about PHP.',
+ 'image' => '/images/logos/phpc-discord.png',
+ 'href' => 'https://discord.phpc.social/',
+ ],
+ [
+ 'title' => 'Official Mailing Lists',
+ 'about' => 'Help and guidance, as well as proposals & discussions on the future of the language.',
+ 'image' => '/images/logos/new-php-logo.png',
+ 'href' => '/mailing-lists.php',
+ ],
+];
diff --git a/include/development-links.inc b/include/development-links.inc
new file mode 100644
index 0000000000..91f36db729
--- /dev/null
+++ b/include/development-links.inc
@@ -0,0 +1,46 @@
+ 'PHP on Github',
+ 'about' => 'Browse and contribute to the source code behind the PHP engine and infrastructure.',
+ 'image' => '/images/logos/github_invertocat_white.svg',
+ 'href' => 'https://github.com/php',
+ 'href_label' => 'Visit GitHub',
+ ],
+ [
+ 'title' => 'RFCs / Language Proposals',
+ 'about' => 'Requests for Comments are the mechanism PHP internals uses to propose language changes.',
+ 'image' => 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/PHP-logo.svg/500px-PHP-logo.svg.png',
+ 'href' => 'https://wiki.php.net/rfc',
+ 'href_label' => 'View Proposals',
+ ],
+ [
+ 'title' => 'PHP Internals (externals.io)',
+ 'about' => 'Browse discussions from PHP Internals about current and future enhancements.',
+ 'image' => '/images/landing/externals-io.svg',
+ 'href' => 'https://externals.io',
+ 'href_label' => 'Browser Externals',
+ ],
+ [
+ 'title' => 'Get Involved',
+ 'about' => 'Find ways to contribute to the PHP engine, documentation and more.',
+ 'image' => '/images/landing/contribute.png',
+ 'href' => '/get-involved.php',
+ 'href_label' => 'Get Involved',
+ ],
+ [
+ 'title' => 'Submit a Bug Report',
+ 'about' => 'Found a bug in the PHP runtime? Help us out by submitting it to our issue tracker.',
+ 'image' => '/images/landing/php-bugs.png',
+ 'href' => 'https://github.com/php/php-src/issues',
+ 'href_label' => 'Browse & Submit Issues',
+ ],
+ [
+ 'title' => 'Documentation Translation',
+ 'about' => 'Help our team translate our documentation into multiple languages.',
+ 'image' => '/images/landing/docs-translator.png',
+ 'href' => 'https://doc.php.net/guide/',
+ 'href_label' => 'Learn About Translation',
+ ]
+];
diff --git a/include/footer.inc b/include/footer.inc
index faa7305d68..7ddcb50a4f 100644
--- a/include/footer.inc
+++ b/include/footer.inc
@@ -1,4 +1,7 @@
-
+
+
+
+
";
diff --git a/include/header.inc b/include/header.inc
index 48722edafa..92156b4d59 100644
--- a/include/header.inc
+++ b/include/header.inc
@@ -62,7 +62,7 @@ if (!isset($config["languages"])) {
- ">
+
@@ -106,7 +106,7 @@ if (!isset($config["languages"])) {
- ">
+
@@ -357,6 +357,13 @@ if (!isset($config["languages"])) {
+
+
+
+
+
+
+
@@ -366,5 +373,7 @@ if (!isset($config["languages"])) {
+
+
diff --git a/include/landing-heros.inc b/include/landing-heros.inc
new file mode 100644
index 0000000000..ed84fb7d3c
--- /dev/null
+++ b/include/landing-heros.inc
@@ -0,0 +1,46 @@
+ 'try-it-now',
+ 'title' => 'Try It Now',
+ 'about' => 'Begin writing PHP code immediately within a browser-based sandbox. No install required.',
+ 'href' => '/sandbox/sandbox.php',
+ 'href_label' => 'Launch Sandbox',
+ ],
+ [
+ 'id' => 'why-use-php',
+ 'title' => 'Why Use PHP?',
+ 'about' => 'Learn why PHP powers the global web - from individual blogs to enormous enterprises.',
+ 'href' => 'https://web-php-pr-1172.preview.thephp.foundation/why-use-php',
+ 'href_label' => 'Discover Why',
+ ],
+ [
+ 'id' => 'learn',
+ 'title' => 'Learn',
+ 'about' => 'Browse the documentation, including extensive tutorials and guidance.',
+ 'href' => '/docs',
+ 'href_label' => 'Browse Docs',
+ ],
+ [
+ 'id' => 'releases',
+ 'title' => 'Releases',
+ 'about' => 'View currently supported PHP runtimes including download logs and highlight features.',
+ 'href' => '#releases',
+ 'href_label' => 'View Runtimes',
+ ],
+ [
+ 'id' => 'community',
+ 'title' => 'Community',
+ 'about' => 'Get involved with the PHP Community via forums, live chat and conferences.',
+ 'href' => '#community',
+ 'href_label' => 'Get Engaged',
+ ],
+ [
+ 'id' => 'language-development',
+ 'title' => 'Language Development',
+ 'about' => 'See how the PHP language works to evolve, and maybe even get involved yourself!',
+ 'href' => '#language-development',
+ 'href_label' => 'Find Out More',
+ ],
+];
diff --git a/include/layout.inc b/include/layout.inc
index b9cc7c0b63..0455508881 100644
--- a/include/layout.inc
+++ b/include/layout.inc
@@ -495,7 +495,6 @@ META
if (isset($_SERVER['BASE_PAGE']) && $shortname = get_shortname($_SERVER["BASE_PAGE"])) {
$shorturl = "https://www.php.net/" . $shortname;
}
-
require __DIR__ . "/header.inc";
}
function site_footer(array $config = []): void
diff --git a/include/prepend.inc b/include/prepend.inc
index b2f3969f11..6409202756 100644
--- a/include/prepend.inc
+++ b/include/prepend.inc
@@ -121,3 +121,8 @@ function google_cse(): void {
echo $cse_snippet;
}
+
+function safe(string $html): string
+{
+ return htmlspecialchars($html, encoding: 'UTF-8');
+}
diff --git a/js/common.js b/js/common.js
index 86b4862fd5..c00fc103eb 100644
--- a/js/common.js
+++ b/js/common.js
@@ -879,3 +879,23 @@ function applyTheme(theme) {
}
applyTheme(savedTheme)
+
+function shuffleImmutableArray(array) {
+ const newArray = [...array];
+ for (let i = newArray.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [newArray[i], newArray[j]] = [newArray[j], newArray[i]];
+ }
+ return newArray;
+}
+
+function shuffleDOMChildrenWithLimit(parent, limit) {
+ const children = Array.from(parent.children);
+ const replacements = shuffleImmutableArray(children.slice(0, limit));
+
+ while (parent.children.length) {
+ parent.removeChild(parent.children[0]);
+ }
+
+ replacements.forEach(n => parent.appendChild(n));
+}
diff --git a/js/sandbox.js b/js/sandbox.js
new file mode 100644
index 0000000000..5c9768426a
--- /dev/null
+++ b/js/sandbox.js
@@ -0,0 +1,41 @@
+import phpBinary from "./php-web.mjs";
+
+export class PHPSandbox {
+ constructor(templateFiles) {
+ this.templateFiles = templateFiles;
+ }
+
+ async execute(files) {
+ let buffer = [];
+ let initializing = true;
+
+ files = {...files, ...this.templateFiles};
+
+ const php = await phpBinary({
+ print(data) {
+ if (initializing) {
+ return;
+ }
+
+ console.log('output', data);
+
+ buffer.push(data);
+ }
+ });
+
+ for (const [filename, content] of Object.entries(files)) {
+ const dir = filename.substring(0, filename.lastIndexOf('/'));
+ if (dir) {
+ php.FS_createPath('/', dir, true, true);
+ }
+
+ php.FS.writeFile('/' + filename, content);
+ }
+
+ initializing = false;
+ php.ccall("phpw_run", null, ["string"], ['require "boot.php";']);
+
+ return JSON.parse(buffer.join(""));
+ }
+}
+
diff --git a/landing.php b/landing.php
new file mode 100644
index 0000000000..1cff587b13
--- /dev/null
+++ b/landing.php
@@ -0,0 +1,483 @@
+ '/language.operators.comparison#language.operators.comparison.ternary',
+ '/??' => '/language.operators.comparison#language.operators.comparison.coalesce',
+ '/??=' => '/language.operators.assignment#language.operators.assignment.other',
+ ];
+ if (isset($shortcuts[$uri])) {
+ header("Location: {$shortcuts[$uri]}");
+ exit;
+ }
+})($_SERVER['REQUEST_URI'] ?? '');
+
+// Get the modification date of this PHP file
+$timestamps = [@getlastmod()];
+
+/*
+ The date of prepend.inc represents the age of ALL
+ included files. Please touch it if you modify any
+ other include file (and the modification affects
+ the display of the index page). The cost of stat'ing
+ them all is prohibitive.
+*/
+$timestamps[] = @filemtime("include/prepend.inc");
+
+// These are the only dynamic parts of the frontpage
+$timestamps[] = @filemtime("include/pregen-confs.inc");
+$timestamps[] = @filemtime("include/pregen-news.inc");
+$timestamps[] = @filemtime("include/version.inc");
+$timestamps[] = @filemtime("js/common.js");
+
+// The latest of these modification dates is our real Last-Modified date
+$timestamp = max($timestamps);
+
+// Note that this is not a RFC 822 date (the tz is always GMT)
+$tsstring = gmdate("D, d M Y H:i:s ", $timestamp) . "GMT";
+
+// Check if the client has the same page cached
+if (isset($_SERVER["HTTP_IF_MODIFIED_SINCE"]) &&
+ ($_SERVER["HTTP_IF_MODIFIED_SINCE"] == $tsstring)) {
+ header("HTTP/1.1 304 Not Modified");
+ exit();
+}
+
+// Inform the user agent what is our last modification date
+header("Last-Modified: " . $tsstring);
+
+$_SERVER['BASE_PAGE'] = 'index.php';
+include_once 'include/prepend.inc';
+include_once 'include/branches.inc';
+include_once 'include/pregen-confs.inc';
+include_once 'include/version.inc';
+
+mirror_setcookie("LAST_NEWS", $_SERVER["REQUEST_TIME"], 60 * 60 * 24 * 365);
+
+$active_branches = get_active_branches();
+$active_branches_sorted = [];
+
+/** @var array}> $branch_descriptors */
+$branch_descriptors = require __DIR__ . '/include/branch-meta.inc';
+
+krsort($active_branches);
+foreach ($active_branches as $major => $releases) {
+ ksort($releases);
+ $releases = array_reverse($releases);
+
+ foreach ($releases as $release) {
+ $version = $release['version'];
+ [$major, $minor, $_] = explode('.', $version);
+ $versionLabel = $major . '.' . $minor;
+ $branch = $branch_descriptors[$versionLabel] ?? [];
+
+ $active_branches_sorted[] = [
+ ...$release,
+ 'major' => $major,
+ 'minor' => $minor,
+ 'version_ex' => $major . '.'. $minor,
+ 'label' => $versionLabel,
+ 'download_url' => '/downloads.php?version=' . $versionLabel,
+ 'more_url' => '/releases/' . $versionLabel . '/en.php',
+ 'changelog_url' => '/ChangeLog-' . $major . '.php#' . $version,
+ 'migration_url' => '/migration' . $major . $minor,
+ 'security_eol' => get_branch_security_eol_date($versionLabel),
+ 'support_eol' => get_branch_bug_eol_date($versionLabel),
+ 'meta' => $branch,
+ 'logo' => '/images/php8/logo_php' . $major . '_' . $minor . '.svg',
+ 'features' => [
+ ...($branch['features'] ?? []),
+ ]
+ ];
+ }
+}
+
+$latest = array_shift($active_branches_sorted);
+
+function buildNavCard(NavCardItem $card, array $config = []): string
+{
+ $config = [
+ 'cn_card' => 'landing-cc-card',
+ 'cn_card_content' => 'landing-cc-card-content',
+ 'cn_card_img' => 'landing-cc-card-img',
+ ...$config,
+ ];
+
+ ob_start();
+ ?>
+ id) { ?>id="= safe($card->id) ?>" } ?> href="= safe($card->href) ?>" class="vgrid-card landing-card-ovh = $config['cn_card'] ?>">
+