@devslab/site-kit 0.11.0 → 0.12.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/README.ko.md CHANGED
@@ -17,6 +17,26 @@ claim leaf가 검증된 사실 레지스트리를 참조하도록 강제한다.
17
17
  기존 environment-only 출력은 유지되며, 선택적 `policies`로 검색 인덱싱,
18
18
  인용 crawler, 모델 학습 crawler를 각각 제어할 수 있다.
19
19
 
20
+ ## 브랜드 아이콘
21
+
22
+ 모든 제품의 아이콘 파일은 `@devslab/linq-brand`(`dist/<product>/`)에서 온다. `brandIconLinks()`는 그중 어떤 파일을 페이지 head가 어떤 순서로 링크하는지 정하는 유일한 자리다.
23
+
24
+ | 링크 | 파일 | 이유 |
25
+ |---|---|---|
26
+ | `icon` `image/svg+xml` | `favicon.svg` | 탭 아이콘. SVG를 아는 브라우저가 래스터를 받지 않도록 맨 앞 |
27
+ | `icon` `48x48` | `mark-48.png` | 검색엔진은 `<link>`로 선언된 48px 이상 정사각형을 원한다(구글: "최소 8x8px, 48x48px 초과 권장") |
28
+ | `icon` `16x16 32x32 48x48` | `favicon.ico` | 주소만으로 요청되는 관례, 세 크기를 한 컨테이너에 |
29
+ | `apple-touch-icon` `180x180` | `apple-touch-icon.png` | iOS 홈 화면 |
30
+
31
+ `BRAND_ICON_FILES`는 같은 네 파일명을 나열하므로 제품은 한 디렉터리에서 그대로 서빙할 수 있다. 기본 `basePath`는 `/brand`이고, 다른 곳에서 서빙하는 제품은 자기 경로를 넘긴다. 같은 오리진 경로만 받는다 — 아이콘은 제품이 직접 서빙한다.
32
+
33
+ ```ts
34
+ toTanStackHead(metadata, { icons: true }); // …/brand/favicon.svg, …
35
+ toTanStackHead(metadata, { icons: { basePath: "/" } }); // /favicon.svg, /mark-48.png, …
36
+ ```
37
+
38
+ 옵션을 생략하면 어댑터는 아이콘 링크를 내지 않는다. 제품이 파일을 어디서 서빙하는지 어댑터가 알 수 없고, 서버가 404를 내는 아이콘을 링크한 head는 아무것도 링크하지 않은 head보다 나쁘기 때문이다.
39
+
20
40
  ## 섹션
21
41
 
22
42
  가족 랜딩 페이지를 위한 상태 없는 원시 컴포넌트 여섯 개, VisionLinq에서 추출했다. `@devslab/site-kit/solid`에서 import한다; 스타일시트는 `styles.css` 안에 들어 있다. 이 원시 컴포넌트들은 full-bleed — 각자 자기 내부 너비를 갖는다 — 라서 이걸로 구성한 페이지는 `<MarketingShell mainWidth="bleed">` 안에서 렌더해야 한다; 그렇지 않으면 셸의 기본 `<main>` 인셋이 이중으로 안쪽 여백을 주고 `tone="band"`가 박스형 사각형이 되어 버린다.
package/README.md CHANGED
@@ -17,6 +17,26 @@ still references the verified-fact registry. `buildRobots` keeps its legacy
17
17
  environment-only output, while an optional `policies` object can independently
18
18
  control search indexing, citation crawlers, and model-training crawlers.
19
19
 
20
+ ## Brand icons
21
+
22
+ Every product's icon files come from `@devslab/linq-brand` (`dist/<product>/`); `brandIconLinks()` is the one place that says which of them a page head links, and in what order:
23
+
24
+ | Link | File | Why |
25
+ |---|---|---|
26
+ | `icon` `image/svg+xml` | `favicon.svg` | the tab icon, first so a browser that understands SVG never fetches a raster |
27
+ | `icon` `48x48` | `mark-48.png` | search engines want a `<link>`-declared square of at least 48px (Google: "at least 8x8px, preferably >48x48px") |
28
+ | `icon` `16x16 32x32 48x48` | `favicon.ico` | the bare-URL convention, three sizes in one container |
29
+ | `apple-touch-icon` `180x180` | `apple-touch-icon.png` | iOS home screen |
30
+
31
+ `BRAND_ICON_FILES` lists the same four names, so a product can serve them from one directory. The default `basePath` is `/brand`; a product that serves the files elsewhere passes its own path. Only same-origin paths are accepted — a product serves its own icons.
32
+
33
+ ```ts
34
+ toTanStackHead(metadata, { icons: true }); // …/brand/favicon.svg, …
35
+ toTanStackHead(metadata, { icons: { basePath: "/" } }); // /favicon.svg, /mark-48.png, …
36
+ ```
37
+
38
+ Omitted, the adapter emits no icon links: it cannot know where a product serves the files, and a head that links icons the server 404s is worse than one that links none.
39
+
20
40
  ## Sections
21
41
 
22
42
  Six stateless primitives for a family landing page, extracted from VisionLinq. Import from `@devslab/site-kit/solid`; the stylesheet ships inside `styles.css`. The primitives are full-bleed — each carries its own inner width — so a page built from them should render inside `<MarketingShell mainWidth="bleed">`; the shell's default `<main>` inset would otherwise double-inset them and turn `tone="band"` into a boxed rectangle.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devslab/site-kit",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
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.0"
63
+ "@devslab/dds-solid": "0.12.0"
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
  }