@websline/cms-view-utils 1.3.3 → 1.5.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@websline/cms-view-utils",
3
- "version": "1.3.3",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -21,13 +21,13 @@
21
21
  "devDependencies": {
22
22
  "@eslint/compat": "^2.1.0",
23
23
  "@eslint/js": "^10.0.1",
24
- "eslint": "^10.5.0",
24
+ "eslint": "^10.7.0",
25
25
  "eslint-config-prettier": "^10.1.8",
26
- "eslint-plugin-svelte": "^3.19.0",
27
- "globals": "^17.6.0",
28
- "prettier": "^3.8.4",
29
- "prettier-plugin-svelte": "^3.5.2",
30
- "prettier-plugin-tailwindcss": "^0.8.0",
26
+ "eslint-plugin-svelte": "^3.22.0",
27
+ "globals": "^17.7.0",
28
+ "prettier": "^3.9.6",
29
+ "prettier-plugin-svelte": "^4.1.1",
30
+ "prettier-plugin-tailwindcss": "^0.8.1",
31
31
  "tsup": "^8.5.1"
32
32
  },
33
33
  "peerDependencies": {
@@ -0,0 +1,9 @@
1
+ const fetchBoard = async ({ locale, boardId }) => {
2
+ const url = `/api/cms/api/public/abm-boards/${locale}/${boardId}`;
3
+
4
+ const response = await fetch(url, { method: "GET" });
5
+
6
+ return response.json();
7
+ };
8
+
9
+ export { fetchBoard };
@@ -0,0 +1,9 @@
1
+ const fetchBoards = async ({ locale }) => {
2
+ const url = `/api/cms/api/public/abm-boards/${locale}`;
3
+
4
+ const response = await fetch(url, { method: "GET" });
5
+
6
+ return response.json();
7
+ };
8
+
9
+ export { fetchBoards };
@@ -31,6 +31,9 @@
31
31
  * from being indexed by search engines.
32
32
  */
33
33
 
34
+ import { fetchSiteConfig } from "../cms/fetchSiteConfig.js";
35
+ import { buildAbsoluteUrl } from "../shared/url.js";
36
+
34
37
  const isPreview = Astro.locals._preview ?? false;
35
38
 
36
39
  const {
@@ -45,6 +48,8 @@ const {
45
48
  } = Astro.props;
46
49
 
47
50
  const {
51
+ alternates: seoAlternates = [],
52
+ mainLocale: seoMainLocale,
48
53
  meta: {
49
54
  canonical: seoCanonical,
50
55
  description: seoDescription,
@@ -67,9 +72,13 @@ const finalCanonical = canonicalOverride ?? seoCanonical;
67
72
  // For image, use explicit undefined check so null can mean "explicitly no image"
68
73
  const finalImage = imageOverride !== undefined ? imageOverride : seoImage;
69
74
 
75
+ // The page's real noindex setting drives canonical/hreflang output, so the
76
+ // preview reflects what production emits (preview only forces the robots meta).
77
+ const pageNoIndex = noIndexOverride ?? seoNoIndex ?? false;
78
+
70
79
  // In preview mode, force noindex/nofollow regardless of page settings
71
80
  // to prevent unpublished drafts from being indexed
72
- const finalNoIndex = isPreview ? true : (noIndexOverride ?? seoNoIndex ?? false);
81
+ const finalNoIndex = isPreview ? true : pageNoIndex;
73
82
  const finalNoFollow = isPreview ? true : (noFollowOverride ?? seoNoFollow ?? false);
74
83
 
75
84
  // Build robots string only if at least one directive is active
@@ -81,6 +90,40 @@ const robotsContent =
81
90
 
82
91
  const finalType = typeOverride ?? "website";
83
92
 
93
+ // hreflang: build absolute alternate URLs from the same source the sitemap uses
94
+ // (siteConfig.url + relative alternate path). Only indexable versions belong in
95
+ // the cluster, and a noindex page emits none. Meaningful only with >1 version.
96
+ let hreflangLinks = [];
97
+ const indexableAlternates = seoAlternates.filter((alternate) => !alternate.noIndex);
98
+ if (!pageNoIndex && indexableAlternates.length > 1) {
99
+ const siteConfig = await fetchSiteConfig(Astro.request).catch(() => null);
100
+ const siteUrl = siteConfig?.url;
101
+
102
+ if (siteUrl) {
103
+ hreflangLinks = indexableAlternates.map((alternate) => ({
104
+ hreflang: alternate.locale,
105
+ href: buildAbsoluteUrl(siteUrl, alternate.url),
106
+ }));
107
+
108
+ // x-default: prefer EN when this page has an indexable EN version,
109
+ // otherwise fall back to the main locale.
110
+ const xDefaultLocale = indexableAlternates.some(
111
+ (alternate) => alternate.locale === "en",
112
+ )
113
+ ? "en"
114
+ : seoMainLocale;
115
+ const xDefaultAlternate = indexableAlternates.find(
116
+ (alternate) => alternate.locale === xDefaultLocale,
117
+ );
118
+ if (xDefaultAlternate) {
119
+ hreflangLinks.push({
120
+ hreflang: "x-default",
121
+ href: buildAbsoluteUrl(siteUrl, xDefaultAlternate.url),
122
+ });
123
+ }
124
+ }
125
+ }
126
+
84
127
  // OG fallbacks (same as meta by default, can be extended via overrides later)
85
128
  const ogTitle = finalTitle;
86
129
  const ogDescription = finalDescription;
@@ -88,7 +131,12 @@ const ogDescription = finalDescription;
88
131
 
89
132
  {finalTitle && <title>{finalTitle}</title>}
90
133
  {finalDescription && <meta name="description" content={finalDescription} />}
91
- {finalCanonical && <link rel="canonical" href={finalCanonical} />}
134
+ {finalCanonical && !pageNoIndex && <link rel="canonical" href={finalCanonical} />}
135
+ {
136
+ hreflangLinks.map((link) => (
137
+ <link rel="alternate" hreflang={link.hreflang} href={link.href} />
138
+ ))
139
+ }
92
140
  {robotsContent && <meta name="robots" content={robotsContent} />}
93
141
 
94
142
  {/* Open Graph */}
@@ -1,3 +1,5 @@
1
+ import { buildAbsoluteUrl } from "../shared/url.js";
2
+
1
3
  const escapeXml = (value) =>
2
4
  String(value).replace(/[<>&'"]/g, (char) => {
3
5
  switch (char) {
@@ -24,7 +26,7 @@ const formatLastmod = (timestamp) => {
24
26
  };
25
27
 
26
28
  const buildUrlBlock = (entry, siteUrl) => {
27
- const loc = encodeUrl(`${siteUrl}${entry.path}`);
29
+ const loc = encodeUrl(buildAbsoluteUrl(siteUrl, entry.path));
28
30
  const lastmod = entry.updatedAt
29
31
  ? `\n <lastmod>${formatLastmod(entry.updatedAt)}</lastmod>`
30
32
  : "";
@@ -32,12 +34,16 @@ const buildUrlBlock = (entry, siteUrl) => {
32
34
  const alternates = (entry.alternates ?? [])
33
35
  .map(
34
36
  (alt) =>
35
- `\n <xhtml:link rel="alternate" hreflang="${escapeXml(alt.locale)}" href="${encodeUrl(`${siteUrl}${alt.path}`)}" />`,
37
+ `\n <xhtml:link rel="alternate" hreflang="${escapeXml(alt.locale)}" href="${encodeUrl(buildAbsoluteUrl(siteUrl, alt.path))}" />`,
36
38
  )
37
39
  .join("");
38
40
 
41
+ const xDefault = entry.xDefaultPath
42
+ ? `\n <xhtml:link rel="alternate" hreflang="x-default" href="${encodeUrl(buildAbsoluteUrl(siteUrl, entry.xDefaultPath))}" />`
43
+ : "";
44
+
39
45
  return ` <url>
40
- <loc>${loc}</loc>${lastmod}${alternates}
46
+ <loc>${loc}</loc>${lastmod}${alternates}${xDefault}
41
47
  </url>`;
42
48
  };
43
49
 
package/src/index.js CHANGED
@@ -3,6 +3,8 @@ export { fetchSimilarRooms } from "./client/fetchSimilarRooms.js";
3
3
  export { fetchSimilarPackages } from "./client/fetchSimilarPackages.js";
4
4
  export { fetchFaqs } from "./client/fetchFaqs.js";
5
5
  export { fetchCollections } from "./client/fetchCollections.js";
6
+ export { fetchBoards } from "./client/fetchBoards.js";
7
+ export { fetchBoard } from "./client/fetchBoard.js";
6
8
 
7
9
  export { default as AddBlockPlaceholder } from "./components/AddBlockPlaceholder.svelte";
8
10
  export { default as BlockWrapper } from "./components/BlockWrapper.svelte";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Joins the public site URL with a relative path into an absolute URL.
3
+ *
4
+ * Keeps URLs free of a trailing slash: the root path ("/") resolves to the
5
+ * bare site URL, so canonical, sitemap and hreflang all use the same form.
6
+ */
7
+ const buildAbsoluteUrl = (siteUrl, path) => {
8
+ const base = siteUrl.replace(/\/+$/, "");
9
+ return path === "/" ? base : `${base}${path}`;
10
+ };
11
+
12
+ export { buildAbsoluteUrl };