diff --git a/public/index.html b/public/index.html
index 9f1fbc5b..42b44967 100644
--- a/public/index.html
+++ b/public/index.html
@@ -7,21 +7,46 @@
-
+
+
+
+
+
+
+
+
+
+
+
Sysnode | Syscoin Sentry Node Dashboard
diff --git a/public/social-card.png b/public/social-card.png
new file mode 100644
index 00000000..3140d888
Binary files /dev/null and b/public/social-card.png differ
diff --git a/src/components/PageMeta.js b/src/components/PageMeta.js
index 246d0fff..bc596382 100644
--- a/src/components/PageMeta.js
+++ b/src/components/PageMeta.js
@@ -1,9 +1,15 @@
import { useEffect } from 'react';
+import { useLocation } from 'react-router-dom';
const SITE_NAME = 'Sysnode';
const DEFAULT_TITLE = 'Sysnode | Syscoin Sentry Node Dashboard';
const DEFAULT_DESCRIPTION =
- 'Track Syscoin Sentry Node count, locked supply, rewards, governance proposals, setup guidance, and market context in one clean dashboard.';
+ 'Live Syscoin Sentry Node stats, governance proposals, rewards, setup guidance, and market data.';
+const SITE_URL = 'https://sysnode.info';
+const SOCIAL_IMAGE_URL = `${SITE_URL}/social-card.png?v=20260518b`;
+const SOCIAL_IMAGE_ALT =
+ 'Sysnode social share card with the Syscoin logo, Sysnode wordmark, and a connected globe network illustration.';
+const TWITTER_HANDLE = '@syscoin';
function ensureMeta(selector, attributeName, attributeValue) {
let element = document.head.querySelector(selector);
@@ -18,8 +24,10 @@ function ensureMeta(selector, attributeName, attributeValue) {
}
export default function PageMeta(props) {
+ const location = useLocation();
const description = props.description || DEFAULT_DESCRIPTION;
const fullTitle = props.title ? `${props.title} | ${SITE_NAME}` : DEFAULT_TITLE;
+ const pageUrl = `${window.location.origin}${location.pathname}${location.search}${location.hash}`;
useEffect(
function syncDocumentMeta() {
@@ -48,12 +56,50 @@ export default function PageMeta(props) {
);
ensureMeta('meta[property="og:url"]', 'property', 'og:url').setAttribute(
'content',
- window.location.href
+ pageUrl
);
+ ensureMeta('meta[property="og:image"]', 'property', 'og:image').setAttribute(
+ 'content',
+ SOCIAL_IMAGE_URL
+ );
+ ensureMeta(
+ 'meta[property="og:image:secure_url"]',
+ 'property',
+ 'og:image:secure_url'
+ ).setAttribute('content', SOCIAL_IMAGE_URL);
+ ensureMeta(
+ 'meta[property="og:image:type"]',
+ 'property',
+ 'og:image:type'
+ ).setAttribute('content', 'image/png');
+ ensureMeta(
+ 'meta[property="og:image:width"]',
+ 'property',
+ 'og:image:width'
+ ).setAttribute('content', '1200');
+ ensureMeta(
+ 'meta[property="og:image:height"]',
+ 'property',
+ 'og:image:height'
+ ).setAttribute('content', '630');
+ ensureMeta(
+ 'meta[property="og:image:alt"]',
+ 'property',
+ 'og:image:alt'
+ ).setAttribute('content', SOCIAL_IMAGE_ALT);
ensureMeta('meta[name="twitter:card"]', 'name', 'twitter:card').setAttribute(
'content',
- 'summary'
+ 'summary_large_image'
);
+ ensureMeta('meta[name="twitter:site"]', 'name', 'twitter:site').setAttribute(
+ 'content',
+ TWITTER_HANDLE
+ );
+ ensureMeta(
+ 'meta[name="twitter:creator"]',
+ 'name',
+ 'twitter:creator'
+ ).setAttribute('content', TWITTER_HANDLE);
ensureMeta('meta[name="twitter:title"]', 'name', 'twitter:title').setAttribute(
'content',
fullTitle
@@ -63,8 +109,17 @@ export default function PageMeta(props) {
'name',
'twitter:description'
).setAttribute('content', description);
+ ensureMeta('meta[name="twitter:image"]', 'name', 'twitter:image').setAttribute(
+ 'content',
+ SOCIAL_IMAGE_URL
+ );
+ ensureMeta(
+ 'meta[name="twitter:image:alt"]',
+ 'name',
+ 'twitter:image:alt'
+ ).setAttribute('content', SOCIAL_IMAGE_ALT);
},
- [description, fullTitle]
+ [description, fullTitle, pageUrl]
);
return null;
diff --git a/src/components/PageMeta.test.js b/src/components/PageMeta.test.js
new file mode 100644
index 00000000..298c90a4
--- /dev/null
+++ b/src/components/PageMeta.test.js
@@ -0,0 +1,52 @@
+import React, { act } from 'react';
+import { render, waitFor } from '@testing-library/react';
+import { Router } from 'react-router-dom';
+import { createMemoryHistory } from 'history';
+
+import PageMeta from './PageMeta';
+
+afterEach(() => {
+ document.title = '';
+ document.head
+ .querySelectorAll(
+ 'meta[name="description"], meta[property^="og:"], meta[name^="twitter:"], link[rel="canonical"]'
+ )
+ .forEach((element) => {
+ element.remove();
+ });
+});
+
+test('updates og:url when only the location changes', async () => {
+ const staticOgUrl = document.createElement('meta');
+ staticOgUrl.setAttribute('property', 'og:url');
+ staticOgUrl.setAttribute('content', 'https://sysnode.info/');
+ document.head.appendChild(staticOgUrl);
+
+ const history = createMemoryHistory({
+ initialEntries: ['/verify-email?token=alpha'],
+ });
+
+ render(
+
+
+
+ );
+
+ expect(document.head.querySelector('meta[property="og:url"]')).toHaveAttribute(
+ 'content',
+ 'http://localhost/verify-email?token=alpha'
+ );
+ expect(document.head.querySelector('link[rel="canonical"]')).toBeNull();
+
+ await act(async () => {
+ history.push('/verify-email?token=beta');
+ });
+
+ await waitFor(() =>
+ expect(document.head.querySelector('meta[property="og:url"]')).toHaveAttribute(
+ 'content',
+ 'http://localhost/verify-email?token=beta'
+ )
+ );
+ expect(document.head.querySelector('link[rel="canonical"]')).toBeNull();
+});
diff --git a/src/pages/Home.js b/src/pages/Home.js
index 356faae7..a777c888 100644
--- a/src/pages/Home.js
+++ b/src/pages/Home.js
@@ -112,7 +112,7 @@ export default function Home() {