diff --git a/assets/og/home.png b/assets/og/home.png new file mode 100644 index 0000000..84136d9 Binary files /dev/null and b/assets/og/home.png differ diff --git a/assets/og/play.png b/assets/og/play.png new file mode 100644 index 0000000..02cc0d9 Binary files /dev/null and b/assets/og/play.png differ diff --git a/assets/og/snippet.png b/assets/og/snippet.png new file mode 100644 index 0000000..f5f589b Binary files /dev/null and b/assets/og/snippet.png differ diff --git a/includes/meta.php b/includes/meta.php index 522f2b4..8df57a8 100644 --- a/includes/meta.php +++ b/includes/meta.php @@ -5,6 +5,9 @@ * - $canonical_url: canonical URL, or false to omit it. When unset, it is https://psalm.dev plus * the request path without query, repeated slashes or a trailing slash (pages with fixed * URLs set it explicitly, since routes are case-insensitive) + * - $og: link preview (Open Graph and Twitter card) values, merged over the defaults below: type, title, + * description, url, image (site path of a PNG), image_width, image_height, image_alt, twitter_card. + * Without a title or description, previews fall back to and $meta_description. */ require_once __DIR__ . '/asset.php'; @@ -13,6 +16,19 @@ $canonical_path = rtrim((string) preg_replace('#/+#', '/', strtok($_SERVER['REQUEST_URI'] ?? '/', '?')), '/'); $canonical_url = 'https://psalm.dev' . ($canonical_path === '' ? '/' : $canonical_path); } + +$og = ($og ?? []) + [ + 'type' => 'website', + 'title' => null, + 'description' => $meta_description ?? null, + 'url' => $canonical_url ?: null, + 'image' => '/assets/og/home.png', + 'image_width' => 1200, + 'image_height' => 630, + 'image_alt' => 'Psalm: Give PHP the love it deserves', + 'twitter_card' => 'summary_large_image', +]; +$og_attr = static fn (string $value): string => htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE); ?> <?php if (!empty($meta_description)): ?> <meta name="description" content="<?= htmlspecialchars($meta_description, ENT_QUOTES) ?>" /> @@ -21,7 +37,24 @@ <link rel="canonical" href="<?= htmlspecialchars($canonical_url, ENT_QUOTES) ?>" /> <?php endif ?> <meta property="og:site_name" content="Psalm" /> -<meta property="og:image" content="https://psalm.dev/psalm_preview.png" /> +<meta property="og:type" content="<?= $og_attr($og['type']) ?>" /> +<?php if ($og['title']): ?> +<meta property="og:title" content="<?= $og_attr($og['title']) ?>" /> +<?php endif ?> +<?php if ($og['description']): ?> +<meta property="og:description" content="<?= $og_attr($og['description']) ?>" /> +<?php endif ?> +<?php if ($og['url']): ?> +<meta property="og:url" content="<?= $og_attr($og['url']) ?>" /> +<?php endif ?> +<meta property="og:image" content="<?= $og_attr('https://psalm.dev' . asset_url($og['image'])) ?>" /> +<meta property="og:image:type" content="image/png" /> +<meta property="og:image:width" content="<?= (int) $og['image_width'] ?>" /> +<meta property="og:image:height" content="<?= (int) $og['image_height'] ?>" /> +<meta property="og:image:alt" content="<?= $og_attr($og['image_alt']) ?>" /> +<meta name="twitter:card" content="<?= $og_attr($og['twitter_card']) ?>" /> +<meta name="twitter:site" content="@psalmphp" /> +<meta name="theme-color" content="#0057b7" /> <link rel="stylesheet" href="<?= asset_url('/assets/css/site.css') ?>"> <?php /* Fonts used above the fold on every page: request them with the stylesheet instead of after it is parsed */ ?> <link rel="preload" href="/extra/national-2-web-bold.woff2" as="font" type="font/woff2" crossorigin> diff --git a/psalm-baseline.xml b/psalm-baseline.xml index cce1d6b..179c030 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -89,7 +89,6 @@ <code><![CDATA[$article]]></code> <code><![CDATA[$article]]></code> <code><![CDATA[$article]]></code> - <code><![CDATA[$article]]></code> </PossiblyUndefinedGlobalVariable> <RiskyTruthyFalsyComparison> <code><![CDATA[$published]]></code> @@ -129,6 +128,7 @@ <UnusedVariable> <code><![CDATA[$meta_description]]></code> <code><![CDATA[$not_found_message]]></code> + <code><![CDATA[$og]]></code> <code><![CDATA[$title]]></code> </UnusedVariable> </file> @@ -187,6 +187,7 @@ <file src="views/index.php"> <UnusedVariable> <code><![CDATA[$canonical_url]]></code> + <code><![CDATA[$og]]></code> </UnusedVariable> </file> <file src="views/issue_redirect.php"> @@ -226,6 +227,7 @@ <code><![CDATA[$canonical_url]]></code> <code><![CDATA[$not_found_message]]></code> <code><![CDATA[$not_found_message]]></code> + <code><![CDATA[$og]]></code> </UnusedVariable> </file> <file src="views/support.php"> diff --git a/psalm_preview.png b/psalm_preview.png deleted file mode 100644 index 02be00d..0000000 Binary files a/psalm_preview.png and /dev/null differ diff --git a/views/article.php b/views/article.php index 853d192..34803c0 100644 --- a/views/article.php +++ b/views/article.php @@ -61,6 +61,17 @@ $meta_description = $description; $timestamp = strtotime($article->date); $published = $timestamp === false ? null : date('c', $timestamp); +$og = [ + 'type' => 'article', + 'title' => $article->title, + 'description' => $description, + 'url' => $canonical_url ?: $url, + 'image' => '/article_thumbnail.png', + 'image_width' => 490, + 'image_height' => 459, + 'image_alt' => 'Psalm', + 'twitter_card' => 'summary', +]; ?> <html lang="<?= $lang ?>"> @@ -74,18 +85,9 @@ <?php endforeach ?> <link rel="alternate" hreflang="x-default" href="<?= $e($alternates['en'] ?? $url) ?>" /> <?php endif ?> -<meta property="og:type" content="article" /> -<meta property="og:title" content="<?= $e($article->title) ?>" /> -<meta property="og:description" content="<?= $e($description) ?>" /> -<meta property="og:url" content="<?= $e($canonical_url ?: $url) ?>" /> <?php if ($published): ?> <meta property="article:published_time" content="<?= $published ?>" /> <?php endif ?> -<meta name="twitter:card" content="summary" /> -<meta name="twitter:site" content="@psalmphp" /> -<meta name="twitter:title" content="<?= $e($article->title) ?>" /> -<meta name="twitter:description" content="<?= $e($description) ?>" /> -<meta name="twitter:image" content="https://psalm.dev/article_thumbnail.png" /> <script type="application/ld+json"> <?= json_encode(array_filter([ '@context' => 'https://schema.org', diff --git a/views/index.php b/views/index.php index de74095..5469b57 100755 --- a/views/index.php +++ b/views/index.php @@ -8,6 +8,15 @@ // /play (?page=play from .htaccess): nav plus the playground filling the viewport, nothing else $playground_page = ($_GET['page'] ?? null) === 'play'; $canonical_url = $playground_page ? 'https://psalm.dev/play' : 'https://psalm.dev/'; +$og = $playground_page ? [ + 'title' => 'Psalm Playground: PHP static analysis in your browser', + 'description' => 'Paste PHP, see type errors and bugs as you type, and share a link to the result.', + 'image' => '/assets/og/play.png', + 'image_alt' => 'Psalm Playground: a PHP function with an InvalidArgument error underlined in red', +] : [ + 'title' => 'Psalm: give PHP the love it deserves', + 'description' => 'Psalm is a free & open-source static analysis tool that helps you identify problems in your PHP code, so you can sleep a little better.', +]; // homepage news: up to three articles from the last two weeks, listed under the playground widget $fresh_articles = $playground_page ? [] : PsalmDotOrg\FreshArticle::recent(dirname(__DIR__) . '/assets/articles'); ?> diff --git a/views/snippet.php b/views/snippet.php index 002fd21..d76a8bc 100644 --- a/views/snippet.php +++ b/views/snippet.php @@ -147,6 +147,14 @@ // Snippets are user-submitted code: crawlable for their links, but kept out of the index header('X-Robots-Tag: noindex, follow'); $canonical_url = false; +// Same preview for every snippet: it never shows the submitted code +$og = [ + 'title' => 'Psalm snippet', + 'description' => 'PHP code analyzed by Psalm, the static analysis tool for PHP. Open it to see the code and the issues Psalm found.', + 'url' => 'https://psalm.dev/r/' . $hash, + 'image' => '/assets/og/snippet.png', + 'image_alt' => 'Psalm code snippet: a PHP function with a PossiblyNullPropertyFetch issue underlined in red', +]; ?> <html lang="en"> <head>