@devslab/site-kit 0.6.0 → 0.8.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": "@devslab/site-kit",
3
- "version": "0.6.0",
3
+ "version": "0.8.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",
@@ -32,6 +32,10 @@
32
32
  "main": "./src/core/index.mjs",
33
33
  "types": "./src/core/index.d.mts",
34
34
  "exports": {
35
+ "./devslab": {
36
+ "types": "./src/core/devslab.d.mts",
37
+ "import": "./src/core/devslab.mjs"
38
+ },
35
39
  ".": {
36
40
  "types": "./src/core/index.d.mts",
37
41
  "import": "./src/core/index.mjs"
@@ -56,7 +60,7 @@
56
60
  "./site-sections.css": "./site-sections.css"
57
61
  },
58
62
  "dependencies": {
59
- "@devslab/dds-solid": "0.6.0"
63
+ "@devslab/dds-solid": "0.8.0"
60
64
  },
61
65
  "peerDependencies": {
62
66
  "@tanstack/solid-router": "1.170.30",
@@ -0,0 +1,2 @@
1
+ import type { PublisherIdentity } from "./publisher.mjs";
2
+ export declare const DEVSLAB_PUBLISHER: Readonly<PublisherIdentity>;
@@ -0,0 +1,12 @@
1
+ import { definePublisher } from "./publisher.mjs";
2
+
3
+ /** Shared DevsLab identity. Product names, capabilities and policies stay with consumers. */
4
+ export const DEVSLAB_PUBLISHER = definePublisher({
5
+ id: "https://devslab.kr/#organization",
6
+ name: "DevsLab",
7
+ alternateName: ["데브스랩", "devslab"],
8
+ url: "https://devslab.kr/",
9
+ sameAs: ["https://github.com/devslab-kr", "https://devslab-kr.github.io/"],
10
+ defaultLabel: "데브스랩(DevsLab)",
11
+ labels: { ko: "데브스랩(DevsLab)", en: "DevsLab" },
12
+ });
@@ -2,3 +2,4 @@ export * from "./locales.mjs";
2
2
  export * from "./catalog.mjs";
3
3
  export * from "./seo.mjs";
4
4
  export * from "./geo.mjs";
5
+ export * from "./publisher.mjs";
@@ -2,3 +2,4 @@ export { FAMILY_LOCALES, LOCALES, canonicalLocale, defineLocaleRegistry, localeA
2
2
  export { CatalogValidationError, createTranslator, validateCatalogs } from "./catalog.mjs";
3
3
  export { ROBOTS_USER_AGENTS, buildMetadata, buildRobots, buildSitemap, localizedPath, localizedUrl, renderSitemapXml } from "./seo.mjs";
4
4
  export { VerifiedFactRegistry, buildVerifiedJsonLd, renderLlmsTxt } from "./geo.mjs";
5
+ export { definePublisher, buildPublisher, serializeJsonLd, renderPublisherHtml } from "./publisher.mjs";
@@ -0,0 +1,27 @@
1
+ export interface PublisherIdentity {
2
+ readonly id: string;
3
+ readonly name: string;
4
+ readonly url: string;
5
+ readonly alternateName?: string | readonly string[];
6
+ readonly sameAs?: readonly string[];
7
+ readonly labels?: Readonly<Record<string, string>>;
8
+ readonly defaultLabel?: string;
9
+ }
10
+ export interface PublisherOrganization {
11
+ "@context": "https://schema.org";
12
+ "@type": "Organization";
13
+ "@id": string;
14
+ name: string;
15
+ url: string;
16
+ alternateName?: string | readonly string[];
17
+ sameAs?: readonly string[];
18
+ }
19
+ export interface BuiltPublisher {
20
+ organization: PublisherOrganization;
21
+ reference: { "@id": string };
22
+ link: { href: string; label: string };
23
+ }
24
+ export declare function definePublisher(input: PublisherIdentity): Readonly<PublisherIdentity>;
25
+ export declare function buildPublisher(identity: PublisherIdentity, options?: { locale?: string }): BuiltPublisher;
26
+ export declare function serializeJsonLd(value: unknown): string;
27
+ export declare function renderPublisherHtml(identity: PublisherIdentity, options?: { locale?: string; nonce?: string }): string;
@@ -0,0 +1,62 @@
1
+ function text(value, field) {
2
+ if (typeof value !== "string" || !value.trim()) throw new TypeError(`Publisher ${field} must be a nonempty string`);
3
+ return value;
4
+ }
5
+
6
+ function webUrl(value, field) {
7
+ text(value, field);
8
+ let parsed;
9
+ try { parsed = new URL(value); } catch { throw new TypeError(`Publisher ${field} must be an absolute HTTP(S) URL`); }
10
+ if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password) throw new TypeError(`Publisher ${field} must be an HTTP(S) URL without credentials`);
11
+ return parsed.href;
12
+ }
13
+
14
+ /** Identity only. Capabilities, endorsements and other GEO claims remain verified facts. */
15
+ export function definePublisher(input) {
16
+ const labels = Object.fromEntries(Object.entries(input.labels ?? {}).map(([locale, label]) => [locale, text(label, "label")]));
17
+ const alternateName = input.alternateName === undefined ? undefined : Array.isArray(input.alternateName)
18
+ ? Object.freeze(input.alternateName.map((name) => text(name, "alternateName"))) : text(input.alternateName, "alternateName");
19
+ const sameAs = input.sameAs === undefined ? undefined : Object.freeze(input.sameAs.map((url) => webUrl(url, "sameAs URL")));
20
+ return Object.freeze({
21
+ id: webUrl(input.id, "id URL"), name: text(input.name, "name"), url: webUrl(input.url, "url URL"),
22
+ ...(alternateName === undefined ? {} : { alternateName }),
23
+ ...(sameAs === undefined ? {} : { sameAs }),
24
+ labels: Object.freeze(labels),
25
+ defaultLabel: text(input.defaultLabel ?? input.name, "defaultLabel"),
26
+ });
27
+ }
28
+
29
+ export function buildPublisher(input, { locale } = {}) {
30
+ const identity = definePublisher(input);
31
+ const language = locale?.toLowerCase();
32
+ const labelFor = (key) => Object.hasOwn(identity.labels, key) ? identity.labels[key] : undefined;
33
+ const label = labelFor(locale) ?? labelFor(language) ?? labelFor(language?.split("-")[0]) ?? identity.defaultLabel;
34
+ return {
35
+ organization: {
36
+ "@context": "https://schema.org", "@type": "Organization", "@id": identity.id,
37
+ name: identity.name, url: identity.url,
38
+ ...(identity.alternateName === undefined ? {} : { alternateName: identity.alternateName }),
39
+ ...(identity.sameAs === undefined ? {} : { sameAs: identity.sameAs }),
40
+ },
41
+ reference: { "@id": identity.id },
42
+ link: { href: identity.url, label },
43
+ };
44
+ }
45
+
46
+ /** Safe inside an HTML script element, including when values contain </script>. */
47
+ export function serializeJsonLd(value) {
48
+ const json = JSON.stringify(value);
49
+ if (json === undefined) throw new TypeError("JSON-LD must be JSON serializable");
50
+ return json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
51
+ }
52
+
53
+ function escapeHtml(value) {
54
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
55
+ }
56
+
57
+ /** Build-time/SSR snippet: a visible attribution link and its matching identity. */
58
+ export function renderPublisherHtml(identity, { locale, nonce } = {}) {
59
+ const { organization, link } = buildPublisher(identity, { locale });
60
+ const nonceAttribute = nonce === undefined ? "" : ` nonce="${escapeHtml(nonce)}"`;
61
+ return `<a href="${escapeHtml(link.href)}">${escapeHtml(link.label)}</a>\n<script type="application/ld+json"${nonceAttribute}>${serializeJsonLd(organization)}</script>`;
62
+ }
package/styles.css CHANGED
@@ -19,6 +19,15 @@
19
19
  .site-locale { inline-size: auto; min-inline-size: 9rem; }
20
20
  .site-footer { border-block-start: 1px solid var(--dds-color-border-default); margin-block-start: var(--dds-space-64); padding-block: var(--dds-space-32); }
21
21
  .site-footer__inner { inline-size: min(100% - var(--dds-space-32), 1120px); margin-inline: auto; display: grid; gap: var(--dds-space-16); }
22
+ .site-footer__langs { display: flex; flex-wrap: wrap; gap: var(--dds-space-8) var(--dds-space-16); font-size: var(--dds-typo-caption-font-size); }
23
+ .site-footer__langs a { color: var(--dds-color-text-muted); text-decoration: none; }
24
+ .site-footer__langs a:hover { color: var(--dds-color-text-primary); }
25
+ .site-footer__langs a[aria-current="page"] { color: var(--dds-color-text-primary); font-weight: 600; }
26
+ .site-footer__row { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: var(--dds-space-16) var(--dds-space-32); }
27
+ .site-footer__brand { display: inline-flex; align-items: center; gap: var(--dds-space-8); margin: 0; color: var(--dds-color-text-secondary); }
28
+ .site-footer__brand img, .site-footer__brand svg { block-size: 16px; inline-size: 16px; }
29
+ .site-footer__brand a { color: inherit; text-decoration: none; }
30
+ .site-footer__brand a:hover { color: var(--dds-color-text-primary); text-decoration: underline; }
22
31
  .site-legal { max-inline-size: 760px; margin-inline: auto; padding-block: var(--dds-space-40); }
23
32
  .site-status { border: 1px solid var(--dds-color-border-default); border-inline-start-width: var(--dds-space-4); border-radius: var(--dds-radius-md); padding: var(--dds-space-12) var(--dds-space-16); }
24
33
  .site-status--danger { border-inline-start-color: var(--dds-color-status-danger); }