@tangle-network/brand 1.26.2 → 1.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +18 -0
- package/dist/social.d.ts +40 -0
- package/dist/social.js +109 -0
- package/package.json +13 -1
- package/src/social.ts +93 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.27.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e652c36: Expose social metadata declarations to TypeScript consumers using legacy Node
|
|
8
|
+
module resolution. The modern package export, runtime code and styles stay the same.
|
|
9
|
+
|
|
10
|
+
## 1.27.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- 3527225: Publish framework-neutral social metadata for server-rendered and static product pages. Keep the poster fallback, canonical URL, alt text and optional video properties together without importing React or claiming social-client playback.
|
|
15
|
+
|
|
3
16
|
## 1.26.2
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -10,6 +10,24 @@ pnpm add @tangle-network/brand
|
|
|
10
10
|
|
|
11
11
|
## Use
|
|
12
12
|
|
|
13
|
+
### Social previews
|
|
14
|
+
|
|
15
|
+
`@tangle-network/brand/social` is framework-neutral. Pass the page's existing
|
|
16
|
+
copy and verified media to `socialMetadata({ url, title, description, image,
|
|
17
|
+
video? })`. The image requires its public HTTPS `url`, MIME `type`, measured
|
|
18
|
+
pixel `width`/`height` and descriptive `alt`; an optional video requires the same
|
|
19
|
+
media fields except alt. Render the returned `tags` in the server/static head
|
|
20
|
+
and use `canonicalUrl` for the canonical link. Each tag has either `property`
|
|
21
|
+
or `name`, plus `content`; the consumer's HTML renderer owns escaping.
|
|
22
|
+
|
|
23
|
+
Keep product-specific composition and media approval with their existing
|
|
24
|
+
owners. Brand owns the common tag contract; callers supply the approved asset
|
|
25
|
+
URLs. The default X card retains the image fallback. Do not infer video playback
|
|
26
|
+
from metadata or switch to a platform-specific player without that platform's
|
|
27
|
+
current supported contract and a real preview check. This helper does not fetch
|
|
28
|
+
media, prove its MIME/codec/duration/range/cache behavior, or approve publication.
|
|
29
|
+
Verify those against the served files. Canonical URLs omit query and fragment.
|
|
30
|
+
|
|
13
31
|
### Tailwind v4
|
|
14
32
|
|
|
15
33
|
```css
|
package/dist/social.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/social.d.ts
|
|
2
|
+
export interface SocialImage {
|
|
3
|
+
url: string;
|
|
4
|
+
type: "image/png" | "image/jpeg" | "image/webp";
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
alt: string;
|
|
8
|
+
}
|
|
9
|
+
export interface SocialVideo {
|
|
10
|
+
url: string;
|
|
11
|
+
type: "video/mp4" | "video/webm";
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
}
|
|
15
|
+
export interface SocialPage {
|
|
16
|
+
url: string;
|
|
17
|
+
title: string;
|
|
18
|
+
description: string;
|
|
19
|
+
image: SocialImage;
|
|
20
|
+
video?: SocialVideo;
|
|
21
|
+
type?: "website" | "article";
|
|
22
|
+
siteName?: string;
|
|
23
|
+
}
|
|
24
|
+
export type SocialMetaTag = {
|
|
25
|
+
property: string;
|
|
26
|
+
content: string;
|
|
27
|
+
name?: never;
|
|
28
|
+
} | {
|
|
29
|
+
name: string;
|
|
30
|
+
content: string;
|
|
31
|
+
property?: never;
|
|
32
|
+
};
|
|
33
|
+
/** Render these tags in the server/static head, never after hydration.
|
|
34
|
+
* Video metadata advertises an asset; a social client's playback needs its own proof.
|
|
35
|
+
*/
|
|
36
|
+
export declare function socialMetadata(page: SocialPage): {
|
|
37
|
+
canonicalUrl: string;
|
|
38
|
+
tags: SocialMetaTag[];
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
package/dist/social.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
//#region src/social.ts
|
|
2
|
+
function httpsUrl(value) {
|
|
3
|
+
const url = new URL(value);
|
|
4
|
+
if (url.protocol !== "https:" || url.username || url.password) throw new Error("Social metadata requires HTTPS URLs without credentials");
|
|
5
|
+
return url;
|
|
6
|
+
}
|
|
7
|
+
function mediaTags(kind, media) {
|
|
8
|
+
const url = httpsUrl(media.url).href;
|
|
9
|
+
if (![media.width, media.height].every((size) => Number.isSafeInteger(size) && size > 0)) throw new Error(`Social ${kind} dimensions must be positive pixel counts`);
|
|
10
|
+
if (!(kind === "image" ? [
|
|
11
|
+
"image/png",
|
|
12
|
+
"image/jpeg",
|
|
13
|
+
"image/webp"
|
|
14
|
+
] : ["video/mp4", "video/webm"]).includes(media.type)) throw new Error(`Unsupported social ${kind} MIME type`);
|
|
15
|
+
return [
|
|
16
|
+
{
|
|
17
|
+
property: `og:${kind}`,
|
|
18
|
+
content: url
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
property: `og:${kind}:secure_url`,
|
|
22
|
+
content: url
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
property: `og:${kind}:type`,
|
|
26
|
+
content: media.type
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
property: `og:${kind}:width`,
|
|
30
|
+
content: String(media.width)
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
property: `og:${kind}:height`,
|
|
34
|
+
content: String(media.height)
|
|
35
|
+
}
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
/** Render these tags in the server/static head, never after hydration.
|
|
39
|
+
* Video metadata advertises an asset; a social client's playback needs its own proof.
|
|
40
|
+
*/
|
|
41
|
+
function socialMetadata(page) {
|
|
42
|
+
for (const text of [
|
|
43
|
+
page.title,
|
|
44
|
+
page.description,
|
|
45
|
+
page.image.alt
|
|
46
|
+
]) if (!text.trim()) throw new Error("Social title, description and image alt must be nonempty");
|
|
47
|
+
const canonical = httpsUrl(page.url);
|
|
48
|
+
canonical.search = "";
|
|
49
|
+
canonical.hash = "";
|
|
50
|
+
const canonicalUrl = canonical.href;
|
|
51
|
+
const imageUrl = httpsUrl(page.image.url).href;
|
|
52
|
+
return {
|
|
53
|
+
canonicalUrl,
|
|
54
|
+
tags: [
|
|
55
|
+
{
|
|
56
|
+
property: "og:type",
|
|
57
|
+
content: page.type ?? "website"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
property: "og:site_name",
|
|
61
|
+
content: page.siteName ?? "Tangle"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
property: "og:url",
|
|
65
|
+
content: canonicalUrl
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
property: "og:title",
|
|
69
|
+
content: page.title
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
property: "og:description",
|
|
73
|
+
content: page.description
|
|
74
|
+
},
|
|
75
|
+
...mediaTags("image", page.image),
|
|
76
|
+
{
|
|
77
|
+
property: "og:image:alt",
|
|
78
|
+
content: page.image.alt
|
|
79
|
+
},
|
|
80
|
+
...page.video ? mediaTags("video", page.video) : [],
|
|
81
|
+
{
|
|
82
|
+
name: "twitter:card",
|
|
83
|
+
content: "summary_large_image"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "twitter:site",
|
|
87
|
+
content: "@tangle_network"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "twitter:title",
|
|
91
|
+
content: page.title
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "twitter:description",
|
|
95
|
+
content: page.description
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "twitter:image",
|
|
99
|
+
content: imageUrl
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "twitter:image:alt",
|
|
103
|
+
content: page.image.alt
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
export { socialMetadata };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/brand",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.1",
|
|
4
4
|
"description": "Tangle brand system — design tokens, logo, Tailwind v4 theme. Single source of truth for every Tangle app.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,6 +27,13 @@
|
|
|
27
27
|
"README.md",
|
|
28
28
|
"CHANGELOG.md"
|
|
29
29
|
],
|
|
30
|
+
"typesVersions": {
|
|
31
|
+
"*": {
|
|
32
|
+
"social": [
|
|
33
|
+
"dist/social.d.ts"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
},
|
|
30
37
|
"exports": {
|
|
31
38
|
".": {
|
|
32
39
|
"types": "./dist/index.d.ts",
|
|
@@ -50,6 +57,11 @@
|
|
|
50
57
|
"import": "./dist/knot.js",
|
|
51
58
|
"default": "./dist/knot.js"
|
|
52
59
|
},
|
|
60
|
+
"./social": {
|
|
61
|
+
"types": "./dist/social.d.ts",
|
|
62
|
+
"import": "./dist/social.js",
|
|
63
|
+
"default": "./dist/social.js"
|
|
64
|
+
},
|
|
53
65
|
"./styles/presentation.css": "./src/styles/presentation.css"
|
|
54
66
|
},
|
|
55
67
|
"peerDependencies": {
|
package/src/social.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export interface SocialImage {
|
|
2
|
+
url: string;
|
|
3
|
+
type: "image/png" | "image/jpeg" | "image/webp";
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
alt: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SocialVideo {
|
|
10
|
+
url: string;
|
|
11
|
+
type: "video/mp4" | "video/webm";
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SocialPage {
|
|
17
|
+
url: string;
|
|
18
|
+
title: string;
|
|
19
|
+
description: string;
|
|
20
|
+
image: SocialImage;
|
|
21
|
+
video?: SocialVideo;
|
|
22
|
+
type?: "website" | "article";
|
|
23
|
+
siteName?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type SocialMetaTag =
|
|
27
|
+
| { property: string; content: string; name?: never }
|
|
28
|
+
| { name: string; content: string; property?: never };
|
|
29
|
+
|
|
30
|
+
function httpsUrl(value: string): URL {
|
|
31
|
+
const url = new URL(value);
|
|
32
|
+
if (url.protocol !== "https:" || url.username || url.password) {
|
|
33
|
+
throw new Error("Social metadata requires HTTPS URLs without credentials");
|
|
34
|
+
}
|
|
35
|
+
return url;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function mediaTags(
|
|
39
|
+
kind: "image" | "video",
|
|
40
|
+
media: SocialImage | SocialVideo,
|
|
41
|
+
): SocialMetaTag[] {
|
|
42
|
+
const url = httpsUrl(media.url).href;
|
|
43
|
+
if (![media.width, media.height].every((size) => Number.isSafeInteger(size) && size > 0)) {
|
|
44
|
+
throw new Error(`Social ${kind} dimensions must be positive pixel counts`);
|
|
45
|
+
}
|
|
46
|
+
const types = kind === "image"
|
|
47
|
+
? ["image/png", "image/jpeg", "image/webp"]
|
|
48
|
+
: ["video/mp4", "video/webm"];
|
|
49
|
+
if (!types.includes(media.type)) throw new Error(`Unsupported social ${kind} MIME type`);
|
|
50
|
+
return [
|
|
51
|
+
{ property: `og:${kind}`, content: url },
|
|
52
|
+
{ property: `og:${kind}:secure_url`, content: url },
|
|
53
|
+
{ property: `og:${kind}:type`, content: media.type },
|
|
54
|
+
{ property: `og:${kind}:width`, content: String(media.width) },
|
|
55
|
+
{ property: `og:${kind}:height`, content: String(media.height) },
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Render these tags in the server/static head, never after hydration.
|
|
60
|
+
* Video metadata advertises an asset; a social client's playback needs its own proof.
|
|
61
|
+
*/
|
|
62
|
+
export function socialMetadata(page: SocialPage): {
|
|
63
|
+
canonicalUrl: string;
|
|
64
|
+
tags: SocialMetaTag[];
|
|
65
|
+
} {
|
|
66
|
+
for (const text of [page.title, page.description, page.image.alt]) {
|
|
67
|
+
if (!text.trim()) throw new Error("Social title, description and image alt must be nonempty");
|
|
68
|
+
}
|
|
69
|
+
const canonical = httpsUrl(page.url);
|
|
70
|
+
canonical.search = "";
|
|
71
|
+
canonical.hash = "";
|
|
72
|
+
const canonicalUrl = canonical.href;
|
|
73
|
+
const imageUrl = httpsUrl(page.image.url).href;
|
|
74
|
+
return {
|
|
75
|
+
canonicalUrl,
|
|
76
|
+
tags: [
|
|
77
|
+
{ property: "og:type", content: page.type ?? "website" },
|
|
78
|
+
{ property: "og:site_name", content: page.siteName ?? "Tangle" },
|
|
79
|
+
{ property: "og:url", content: canonicalUrl },
|
|
80
|
+
{ property: "og:title", content: page.title },
|
|
81
|
+
{ property: "og:description", content: page.description },
|
|
82
|
+
...mediaTags("image", page.image),
|
|
83
|
+
{ property: "og:image:alt", content: page.image.alt },
|
|
84
|
+
...(page.video ? mediaTags("video", page.video) : []),
|
|
85
|
+
{ name: "twitter:card", content: "summary_large_image" },
|
|
86
|
+
{ name: "twitter:site", content: "@tangle_network" },
|
|
87
|
+
{ name: "twitter:title", content: page.title },
|
|
88
|
+
{ name: "twitter:description", content: page.description },
|
|
89
|
+
{ name: "twitter:image", content: imageUrl },
|
|
90
|
+
{ name: "twitter:image:alt", content: page.image.alt },
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|