@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 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
@@ -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.26.2",
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
+ }