@devslab/site-kit 0.11.1 → 0.12.2

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": "@devslab/site-kit",
3
- "version": "0.11.1",
3
+ "version": "0.12.2",
4
4
  "description": "Strict i18n, SEO/GEO, and Solid public-site shells for DevsLab products",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -60,7 +60,7 @@
60
60
  "./site-sections.css": "./site-sections.css"
61
61
  },
62
62
  "dependencies": {
63
- "@devslab/dds-solid": "0.11.1"
63
+ "@devslab/dds-solid": "0.12.2"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "@tanstack/solid-router": "1.170.30",
@@ -1,5 +1,5 @@
1
1
  export { FAMILY_LOCALES, LOCALES, canonicalLocale, defineLocaleRegistry, localeAttributes, resolveLocale } from "./locales.mjs";
2
2
  export { CatalogValidationError, createTranslator, validateCatalogs } from "./catalog.mjs";
3
- export { ROBOTS_USER_AGENTS, buildMetadata, buildRobots, buildSitemap, localizedPath, localizedUrl, renderSitemapXml } from "./seo.mjs";
3
+ export { BRAND_ICON_FILES, ROBOTS_USER_AGENTS, brandIconLinks, buildMetadata, buildRobots, buildSitemap, localizedPath, localizedUrl, renderSitemapXml } from "./seo.mjs";
4
4
  export { VerifiedFactRegistry, buildVerifiedJsonLd, renderLlmsTxt } from "./geo.mjs";
5
5
  export { definePublisher, buildPublisher, serializeJsonLd, renderPublisherHtml } from "./publisher.mjs";
@@ -15,3 +15,8 @@ export declare function renderSitemapXml(entries: ReturnType<typeof buildSitemap
15
15
  export type RobotsPolicy = "allow" | "disallow";
16
16
  export declare const ROBOTS_USER_AGENTS: { readonly citation: readonly string[]; readonly modelTraining: readonly string[] };
17
17
  export declare function buildRobots(input: { baseUrl: string; environment: "production" | "preview" | "development"; policies?: { search: RobotsPolicy; citation: RobotsPolicy; modelTraining: RobotsPolicy } }): string;
18
+ /** The @devslab/linq-brand icon files a page head links, in link order. */
19
+ export declare const BRAND_ICON_FILES: readonly ["favicon.svg", "mark-48.png", "favicon.ico", "apple-touch-icon.png"];
20
+ export interface BrandIconLink { rel: "icon" | "apple-touch-icon"; href: string; type?: string; sizes?: string }
21
+ /** `<link>` descriptors for BRAND_ICON_FILES served at `basePath` (default "/brand"; a same-origin path, never a URL). */
22
+ export declare function brandIconLinks(options?: { basePath?: string }): BrandIconLink[];
package/src/core/seo.mjs CHANGED
@@ -92,3 +92,46 @@ export function buildRobots({ baseUrl, environment, policies }) {
92
92
  if (environment !== "production") return "User-agent: *\nDisallow: /\n";
93
93
  return `User-agent: *\nAllow: /\nSitemap: ${cleanBase(baseUrl)}/sitemap.xml\n`;
94
94
  }
95
+
96
+ /**
97
+ * The icon files @devslab/linq-brand ships for every product under
98
+ * dist/<product>/, in the order a page head should link them. A product
99
+ * serves that directory as-is (VisionLinq, BookLinq: /brand/*) or vendors
100
+ * the bytes and serves them under the same names (AskLinq); either way the
101
+ * head links come from here, so the four family sites cannot each pick a
102
+ * different subset — which is what they did before this existed.
103
+ *
104
+ * favicon.svg — the tab icon, crisp at every size, linked first so a
105
+ * browser that understands it never fetches a raster.
106
+ * mark-48.png — one raster of at least 48px: search engines want a
107
+ * <link>-declared square, Google "at least 8x8px,
108
+ * preferably >48x48px", and a 32px-only site shows its
109
+ * previous icon in results long after the file changed.
110
+ * favicon.ico — 16/32/48 in one container, the bare-URL convention.
111
+ * apple-touch-icon.png — 180px, iOS home screen.
112
+ */
113
+ export const BRAND_ICON_FILES = Object.freeze(["favicon.svg", "mark-48.png", "favicon.ico", "apple-touch-icon.png"]);
114
+
115
+ const iconBase = (basePath) => {
116
+ const value = String(basePath ?? "/brand");
117
+ if (/^[a-z][a-z0-9+.-]*:|^\/\//i.test(value)) {
118
+ throw new RangeError("brandIconLinks basePath is a same-origin path, not a URL: a product serves its own icons");
119
+ }
120
+ const trimmed = value.replace(/^\/+|\/+$/g, "");
121
+ return trimmed ? `/${trimmed}` : "";
122
+ };
123
+
124
+ /**
125
+ * `<link>` descriptors for BRAND_ICON_FILES at `basePath` (default /brand).
126
+ * Same shape as toTanStackHead's `links`; a non-Start renderer can print
127
+ * them as attributes in this order.
128
+ */
129
+ export function brandIconLinks({ basePath = "/brand" } = {}) {
130
+ const base = iconBase(basePath);
131
+ return [
132
+ { rel: "icon", type: "image/svg+xml", href: `${base}/favicon.svg` },
133
+ { rel: "icon", type: "image/png", sizes: "48x48", href: `${base}/mark-48.png` },
134
+ { rel: "icon", sizes: "16x16 32x32 48x48", href: `${base}/favicon.ico` },
135
+ { rel: "apple-touch-icon", sizes: "180x180", href: `${base}/apple-touch-icon.png` },
136
+ ];
137
+ }
@@ -6,5 +6,7 @@ import type { SiteLocale } from "./core/locales.mjs";
6
6
  * registry (D-018) yields `SiteMetadata<string>`; the adapter reads the
7
7
  * same fields whatever the code type is, so it must not refuse that.
8
8
  */
9
- export declare function toTanStackHead<Code extends string = SiteLocale>(metadata: SiteMetadata<Code>): { meta: Array<Record<string, string>>; links: Array<Record<string, string>> };
9
+ /** `icons: true` appends brandIconLinks() (the /brand default); an object passes its options through. Omitted, no icon links are emitted. */
10
+ export interface TanStackHeadOptions { icons?: boolean | { basePath?: string } }
11
+ export declare function toTanStackHead<Code extends string = SiteLocale>(metadata: SiteMetadata<Code>, options?: TanStackHeadOptions): { meta: Array<Record<string, string>>; links: Array<Record<string, string>> };
10
12
  export declare const toHtmlAttributes: <Code extends string = SiteLocale>(metadata: SiteMetadata<Code>) => { lang: Code; dir: string };
@@ -1,4 +1,10 @@
1
- export function toTanStackHead(metadata) {
1
+ import { brandIconLinks } from "./core/seo.mjs";
2
+
3
+ // `icons` is opt-in: the adapter cannot know where (or whether) a product
4
+ // serves the linq-brand files, and a head that links icons the server 404s
5
+ // is worse than one that links none. `true` takes the /brand default.
6
+ export function toTanStackHead(metadata, options = {}) {
7
+ const icons = options.icons === true ? brandIconLinks() : options.icons ? brandIconLinks(options.icons) : [];
2
8
  return {
3
9
  meta: [
4
10
  { title: metadata.title },
@@ -18,6 +24,7 @@ export function toTanStackHead(metadata) {
18
24
  links: [
19
25
  { rel: "canonical", href: metadata.canonical },
20
26
  ...metadata.alternates.map(({ hreflang, href }) => ({ rel: "alternate", hreflang, href })),
27
+ ...icons,
21
28
  ],
22
29
  };
23
30
  }