@hua-labs/hua-ux 0.1.0-alpha.0.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.
Files changed (210) hide show
  1. package/README.md +839 -0
  2. package/dist/framework/a11y/components/LiveRegion.d.ts +64 -0
  3. package/dist/framework/a11y/components/LiveRegion.d.ts.map +1 -0
  4. package/dist/framework/a11y/components/LiveRegion.js +43 -0
  5. package/dist/framework/a11y/components/SkipToContent.d.ts +62 -0
  6. package/dist/framework/a11y/components/SkipToContent.d.ts.map +1 -0
  7. package/dist/framework/a11y/components/SkipToContent.js +60 -0
  8. package/dist/framework/a11y/hooks/useFocusManagement.d.ts +60 -0
  9. package/dist/framework/a11y/hooks/useFocusManagement.d.ts.map +1 -0
  10. package/dist/framework/a11y/hooks/useFocusManagement.js +71 -0
  11. package/dist/framework/a11y/hooks/useFocusTrap.d.ts +64 -0
  12. package/dist/framework/a11y/hooks/useFocusTrap.d.ts.map +1 -0
  13. package/dist/framework/a11y/hooks/useFocusTrap.js +185 -0
  14. package/dist/framework/a11y/hooks/useLiveRegion.d.ts +56 -0
  15. package/dist/framework/a11y/hooks/useLiveRegion.d.ts.map +1 -0
  16. package/dist/framework/a11y/hooks/useLiveRegion.js +60 -0
  17. package/dist/framework/a11y/index.d.ts +16 -0
  18. package/dist/framework/a11y/index.d.ts.map +1 -0
  19. package/dist/framework/a11y/index.js +11 -0
  20. package/dist/framework/branding/context.d.ts +52 -0
  21. package/dist/framework/branding/context.d.ts.map +1 -0
  22. package/dist/framework/branding/context.js +96 -0
  23. package/dist/framework/branding/css-vars.d.ts +34 -0
  24. package/dist/framework/branding/css-vars.d.ts.map +1 -0
  25. package/dist/framework/branding/css-vars.js +95 -0
  26. package/dist/framework/branding/tailwind-config.d.ts +38 -0
  27. package/dist/framework/branding/tailwind-config.d.ts.map +1 -0
  28. package/dist/framework/branding/tailwind-config.js +66 -0
  29. package/dist/framework/components/BrandedButton.d.ts +53 -0
  30. package/dist/framework/components/BrandedButton.d.ts.map +1 -0
  31. package/dist/framework/components/BrandedButton.js +40 -0
  32. package/dist/framework/components/BrandedCard.d.ts +52 -0
  33. package/dist/framework/components/BrandedCard.d.ts.map +1 -0
  34. package/dist/framework/components/BrandedCard.js +73 -0
  35. package/dist/framework/components/ErrorBoundary.d.ts +92 -0
  36. package/dist/framework/components/ErrorBoundary.d.ts.map +1 -0
  37. package/dist/framework/components/ErrorBoundary.js +121 -0
  38. package/dist/framework/components/HuaUxLayout.d.ts +29 -0
  39. package/dist/framework/components/HuaUxLayout.d.ts.map +1 -0
  40. package/dist/framework/components/HuaUxLayout.js +32 -0
  41. package/dist/framework/components/HuaUxPage.d.ts +48 -0
  42. package/dist/framework/components/HuaUxPage.d.ts.map +1 -0
  43. package/dist/framework/components/HuaUxPage.js +105 -0
  44. package/dist/framework/components/Providers.d.ts +17 -0
  45. package/dist/framework/components/Providers.d.ts.map +1 -0
  46. package/dist/framework/components/Providers.js +72 -0
  47. package/dist/framework/components/WelcomePage.d.ts +44 -0
  48. package/dist/framework/components/WelcomePage.d.ts.map +1 -0
  49. package/dist/framework/components/WelcomePage.js +80 -0
  50. package/dist/framework/config/index.d.ts +182 -0
  51. package/dist/framework/config/index.d.ts.map +1 -0
  52. package/dist/framework/config/index.js +329 -0
  53. package/dist/framework/config/merge.d.ts +26 -0
  54. package/dist/framework/config/merge.d.ts.map +1 -0
  55. package/dist/framework/config/merge.js +160 -0
  56. package/dist/framework/config/schema.d.ts +25 -0
  57. package/dist/framework/config/schema.d.ts.map +1 -0
  58. package/dist/framework/config/schema.js +122 -0
  59. package/dist/framework/hooks/useMotion.d.ts +45 -0
  60. package/dist/framework/hooks/useMotion.d.ts.map +1 -0
  61. package/dist/framework/hooks/useMotion.js +40 -0
  62. package/dist/framework/index.d.ts +37 -0
  63. package/dist/framework/index.d.ts.map +1 -0
  64. package/dist/framework/index.js +42 -0
  65. package/dist/framework/license/errors.d.ts +15 -0
  66. package/dist/framework/license/errors.d.ts.map +1 -0
  67. package/dist/framework/license/errors.js +52 -0
  68. package/dist/framework/license/index.d.ts +70 -0
  69. package/dist/framework/license/index.d.ts.map +1 -0
  70. package/dist/framework/license/index.js +124 -0
  71. package/dist/framework/license/loader.d.ts +26 -0
  72. package/dist/framework/license/loader.d.ts.map +1 -0
  73. package/dist/framework/license/loader.js +137 -0
  74. package/dist/framework/license/types.d.ts +67 -0
  75. package/dist/framework/license/types.d.ts.map +1 -0
  76. package/dist/framework/license/types.js +18 -0
  77. package/dist/framework/loading/components/SkeletonGroup.d.ts +44 -0
  78. package/dist/framework/loading/components/SkeletonGroup.d.ts.map +1 -0
  79. package/dist/framework/loading/components/SkeletonGroup.js +34 -0
  80. package/dist/framework/loading/components/SuspenseWrapper.d.ts +58 -0
  81. package/dist/framework/loading/components/SuspenseWrapper.d.ts.map +1 -0
  82. package/dist/framework/loading/components/SuspenseWrapper.js +40 -0
  83. package/dist/framework/loading/hoc/withSuspense.d.ts +46 -0
  84. package/dist/framework/loading/hoc/withSuspense.d.ts.map +1 -0
  85. package/dist/framework/loading/hoc/withSuspense.js +54 -0
  86. package/dist/framework/loading/hooks/useDelayedLoading.d.ts +56 -0
  87. package/dist/framework/loading/hooks/useDelayedLoading.d.ts.map +1 -0
  88. package/dist/framework/loading/hooks/useDelayedLoading.js +97 -0
  89. package/dist/framework/loading/hooks/useLoadingState.d.ts +69 -0
  90. package/dist/framework/loading/hooks/useLoadingState.d.ts.map +1 -0
  91. package/dist/framework/loading/hooks/useLoadingState.js +59 -0
  92. package/dist/framework/loading/index.d.ts +16 -0
  93. package/dist/framework/loading/index.d.ts.map +1 -0
  94. package/dist/framework/loading/index.js +13 -0
  95. package/dist/framework/middleware/i18n.d.ts +90 -0
  96. package/dist/framework/middleware/i18n.d.ts.map +1 -0
  97. package/dist/framework/middleware/i18n.js +99 -0
  98. package/dist/framework/plugins/index.d.ts +8 -0
  99. package/dist/framework/plugins/index.d.ts.map +1 -0
  100. package/dist/framework/plugins/index.js +6 -0
  101. package/dist/framework/plugins/registry.d.ts +95 -0
  102. package/dist/framework/plugins/registry.d.ts.map +1 -0
  103. package/dist/framework/plugins/registry.js +160 -0
  104. package/dist/framework/plugins/types.d.ts +97 -0
  105. package/dist/framework/plugins/types.d.ts.map +1 -0
  106. package/dist/framework/plugins/types.js +6 -0
  107. package/dist/framework/seo/geo/examples.d.ts +87 -0
  108. package/dist/framework/seo/geo/examples.d.ts.map +1 -0
  109. package/dist/framework/seo/geo/examples.js +295 -0
  110. package/dist/framework/seo/geo/generateGEOMetadata.d.ts +107 -0
  111. package/dist/framework/seo/geo/generateGEOMetadata.d.ts.map +1 -0
  112. package/dist/framework/seo/geo/generateGEOMetadata.js +404 -0
  113. package/dist/framework/seo/geo/index.d.ts +19 -0
  114. package/dist/framework/seo/geo/index.d.ts.map +1 -0
  115. package/dist/framework/seo/geo/index.js +21 -0
  116. package/dist/framework/seo/geo/presets.d.ts +52 -0
  117. package/dist/framework/seo/geo/presets.d.ts.map +1 -0
  118. package/dist/framework/seo/geo/presets.js +47 -0
  119. package/dist/framework/seo/geo/structuredData.d.ts +187 -0
  120. package/dist/framework/seo/geo/structuredData.d.ts.map +1 -0
  121. package/dist/framework/seo/geo/structuredData.js +354 -0
  122. package/dist/framework/seo/geo/test-utils.d.ts +78 -0
  123. package/dist/framework/seo/geo/test-utils.d.ts.map +1 -0
  124. package/dist/framework/seo/geo/test-utils.js +139 -0
  125. package/dist/framework/seo/geo/types.d.ts +225 -0
  126. package/dist/framework/seo/geo/types.d.ts.map +1 -0
  127. package/dist/framework/seo/geo/types.js +51 -0
  128. package/dist/framework/types/index.d.ts +577 -0
  129. package/dist/framework/types/index.d.ts.map +1 -0
  130. package/dist/framework/types/index.js +6 -0
  131. package/dist/framework/utils/data-fetching.d.ts +45 -0
  132. package/dist/framework/utils/data-fetching.d.ts.map +1 -0
  133. package/dist/framework/utils/data-fetching.js +74 -0
  134. package/dist/framework/utils/file-structure.d.ts +29 -0
  135. package/dist/framework/utils/file-structure.d.ts.map +1 -0
  136. package/dist/framework/utils/file-structure.js +72 -0
  137. package/dist/framework/utils/metadata.d.ts +109 -0
  138. package/dist/framework/utils/metadata.d.ts.map +1 -0
  139. package/dist/framework/utils/metadata.js +105 -0
  140. package/dist/index.d.ts +15 -0
  141. package/dist/index.d.ts.map +1 -0
  142. package/dist/index.js +21 -0
  143. package/dist/presets/index.d.ts +8 -0
  144. package/dist/presets/index.d.ts.map +1 -0
  145. package/dist/presets/index.js +7 -0
  146. package/dist/presets/marketing.d.ts +41 -0
  147. package/dist/presets/marketing.d.ts.map +1 -0
  148. package/dist/presets/marketing.js +81 -0
  149. package/dist/presets/product.d.ts +41 -0
  150. package/dist/presets/product.d.ts.map +1 -0
  151. package/dist/presets/product.js +74 -0
  152. package/package.json +91 -0
  153. package/src/framework/README.md +329 -0
  154. package/src/framework/__tests__/branding/css-vars.test.ts +147 -0
  155. package/src/framework/__tests__/components/ErrorBoundary.test.tsx +146 -0
  156. package/src/framework/__tests__/config/defineConfig.test.ts +138 -0
  157. package/src/framework/__tests__/hooks/useMotion.test.ts +105 -0
  158. package/src/framework/__tests__/seo/geo/generateGEOMetadata.test.ts +207 -0
  159. package/src/framework/__tests__/seo/geo/structuredData.test.ts +262 -0
  160. package/src/framework/a11y/components/LiveRegion.tsx +89 -0
  161. package/src/framework/a11y/components/SkipToContent.tsx +103 -0
  162. package/src/framework/a11y/hooks/useFocusManagement.ts +125 -0
  163. package/src/framework/a11y/hooks/useFocusTrap.ts +239 -0
  164. package/src/framework/a11y/hooks/useLiveRegion.ts +95 -0
  165. package/src/framework/a11y/index.ts +17 -0
  166. package/src/framework/branding/context.tsx +135 -0
  167. package/src/framework/branding/css-vars.ts +110 -0
  168. package/src/framework/branding/tailwind-config.ts +90 -0
  169. package/src/framework/components/BrandedButton.tsx +94 -0
  170. package/src/framework/components/BrandedCard.tsx +87 -0
  171. package/src/framework/components/ErrorBoundary.tsx +215 -0
  172. package/src/framework/components/HuaUxLayout.tsx +36 -0
  173. package/src/framework/components/HuaUxPage.tsx +138 -0
  174. package/src/framework/components/Providers.tsx +98 -0
  175. package/src/framework/components/WelcomePage.tsx +207 -0
  176. package/src/framework/config/index.ts +349 -0
  177. package/src/framework/config/merge.ts +190 -0
  178. package/src/framework/config/schema.ts +140 -0
  179. package/src/framework/hooks/useMotion.ts +57 -0
  180. package/src/framework/index.ts +122 -0
  181. package/src/framework/license/errors.ts +63 -0
  182. package/src/framework/license/index.ts +137 -0
  183. package/src/framework/license/loader.ts +158 -0
  184. package/src/framework/license/types.ts +95 -0
  185. package/src/framework/loading/components/SkeletonGroup.tsx +70 -0
  186. package/src/framework/loading/components/SuspenseWrapper.tsx +88 -0
  187. package/src/framework/loading/hoc/withSuspense.tsx +96 -0
  188. package/src/framework/loading/hooks/useDelayedLoading.ts +127 -0
  189. package/src/framework/loading/hooks/useLoadingState.ts +103 -0
  190. package/src/framework/loading/index.ts +19 -0
  191. package/src/framework/middleware/i18n.ts +161 -0
  192. package/src/framework/middleware/index.ts +7 -0
  193. package/src/framework/plugins/index.ts +13 -0
  194. package/src/framework/plugins/registry.ts +186 -0
  195. package/src/framework/plugins/types.ts +106 -0
  196. package/src/framework/seo/geo/examples.tsx +415 -0
  197. package/src/framework/seo/geo/generateGEOMetadata.ts +441 -0
  198. package/src/framework/seo/geo/index.ts +61 -0
  199. package/src/framework/seo/geo/presets.ts +58 -0
  200. package/src/framework/seo/geo/structuredData.ts +422 -0
  201. package/src/framework/seo/geo/test-utils.ts +179 -0
  202. package/src/framework/seo/geo/types.ts +315 -0
  203. package/src/framework/types/index.ts +623 -0
  204. package/src/framework/utils/data-fetching.ts +95 -0
  205. package/src/framework/utils/file-structure.ts +88 -0
  206. package/src/framework/utils/metadata.ts +152 -0
  207. package/src/index.ts +31 -0
  208. package/src/presets/index.ts +8 -0
  209. package/src/presets/marketing.ts +88 -0
  210. package/src/presets/product.ts +81 -0
@@ -0,0 +1,441 @@
1
+ /**
2
+ * @hua-labs/hua-ux/framework - GEO Metadata Generator
3
+ *
4
+ * Generate AI-friendly metadata for Generative Engine Optimization (GEO)
5
+ * ChatGPT, Claude, Gemini, Perplexity가 hua-ux를 잘 찾고 추천하도록 메타데이터 생성
6
+ */
7
+
8
+ import type { GEOConfig, GEOMetadata } from './types';
9
+ import { META_NAMES, OG_PROPERTIES } from './types';
10
+ import { generateSoftwareApplicationLD } from './structuredData';
11
+
12
+ /**
13
+ * Generate GEO (Generative Engine Optimization) Metadata
14
+ *
15
+ * AI 검색 엔진이 소프트웨어를 정확하게 이해하고 추천할 수 있도록
16
+ * 구조화된 메타데이터를 생성합니다.
17
+ *
18
+ * Generate structured metadata that helps AI search engines (ChatGPT, Claude,
19
+ * Gemini, Perplexity) accurately understand and recommend your software.
20
+ *
21
+ * @param config - GEO configuration
22
+ * @returns Object containing:
23
+ * - `meta`: Array of HTML meta tags with name and content
24
+ * - `jsonLd`: Array of Schema.org JSON-LD structured data objects
25
+ * - `openGraph`: Array of Open Graph meta tags (optional)
26
+ * - `twitter`: Array of Twitter Card meta tags (optional)
27
+ *
28
+ * @throws {Error} If config.name or config.description is empty
29
+ * @throws {Error} If config.url is provided but invalid
30
+ * @throws {Error} If config.codeRepository is provided but invalid
31
+ * @throws {Error} If config.documentationUrl is provided but invalid
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * // Basic usage
36
+ * const geoMetadata = generateGEOMetadata({
37
+ * name: 'hua-ux',
38
+ * description: 'Privacy-first UX framework for Next.js with built-in i18n, motion, and accessibility',
39
+ * version: '1.0.0',
40
+ * applicationCategory: ['UX Framework', 'Developer Tool'],
41
+ * programmingLanguage: ['TypeScript', 'React', 'Next.js'],
42
+ * features: [
43
+ * 'Privacy-first architecture',
44
+ * 'Built-in internationalization (i18n)',
45
+ * 'Motion animations with hua-motion',
46
+ * 'WCAG 2.1 compliant accessibility',
47
+ * 'Automatic error handling',
48
+ * 'Loading state optimization',
49
+ * ],
50
+ * useCases: [
51
+ * 'Building multilingual Next.js applications',
52
+ * 'Creating accessible web applications',
53
+ * 'Rapid prototyping with AI-friendly documentation',
54
+ * ],
55
+ * keywords: [
56
+ * 'nextjs',
57
+ * 'react',
58
+ * 'ux',
59
+ * 'i18n',
60
+ * 'internationalization',
61
+ * 'accessibility',
62
+ * 'a11y',
63
+ * 'motion',
64
+ * 'animation',
65
+ * 'privacy',
66
+ * ],
67
+ * codeRepository: 'https://github.com/hua-labs/hua',
68
+ * documentationUrl: 'https://hua-labs.dev/docs/hua-ux',
69
+ * license: 'MIT',
70
+ * author: {
71
+ * name: 'hua-labs',
72
+ * url: 'https://hua-labs.dev',
73
+ * },
74
+ * });
75
+ * ```
76
+ *
77
+ * @example
78
+ * ```tsx
79
+ * // Use with Next.js metadata
80
+ * import { generateGEOMetadata } from '@hua-labs/hua-ux/framework';
81
+ *
82
+ * export async function generateMetadata() {
83
+ * const geoMeta = generateGEOMetadata({
84
+ * name: 'My App',
85
+ * description: 'Built with hua-ux',
86
+ * features: ['i18n', 'Dark mode', 'Responsive'],
87
+ * });
88
+ *
89
+ * return {
90
+ * title: 'My App',
91
+ * description: geoMeta.meta.find(m => m.name === 'description')?.content,
92
+ * // Add JSON-LD to page
93
+ * other: {
94
+ * 'script:ld+json': JSON.stringify(geoMeta.jsonLd),
95
+ * },
96
+ * };
97
+ * }
98
+ * ```
99
+ */
100
+
101
+ /**
102
+ * Validate URL format
103
+ */
104
+ function isValidUrl(url: string): boolean {
105
+ try {
106
+ new URL(url);
107
+ return true;
108
+ } catch {
109
+ return false;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Normalize array (single value or array to array)
115
+ */
116
+ function normalizeToArray<T>(value: T | T[] | undefined): T[] {
117
+ if (value === undefined) return [];
118
+ return Array.isArray(value) ? value : [value];
119
+ }
120
+
121
+ /**
122
+ * Join non-empty array values
123
+ */
124
+ function joinNonEmpty(values: string[] | undefined, separator: string): string | undefined {
125
+ if (!values || values.length === 0) return undefined;
126
+ const nonEmpty = values.filter(v => v && v.trim().length > 0);
127
+ return nonEmpty.length > 0 ? nonEmpty.join(separator) : undefined;
128
+ }
129
+
130
+ /**
131
+ * Convert meta tags array to object
132
+ *
133
+ * 메타 태그 배열을 객체로 변환 (Next.js metadata API에서 사용)
134
+ *
135
+ * @param meta - Array of meta tags
136
+ * @returns Object with meta tag names as keys
137
+ *
138
+ * @example
139
+ * ```tsx
140
+ * const metaObj = metaToObject(geoMeta.meta);
141
+ * return {
142
+ * title: 'My App',
143
+ * description: metaObj.description,
144
+ * keywords: metaObj.keywords,
145
+ * };
146
+ * ```
147
+ */
148
+ export function metaToObject(meta: GEOMetadata['meta']): Record<string, string> {
149
+ return Object.fromEntries(meta.map(m => [m.name, m.content]));
150
+ }
151
+
152
+ /**
153
+ * Convert Open Graph tags array to object
154
+ *
155
+ * Open Graph 태그 배열을 객체로 변환
156
+ *
157
+ * @param og - Array of Open Graph tags
158
+ * @returns Object with Open Graph properties as keys
159
+ *
160
+ * @example
161
+ * ```tsx
162
+ * const ogObj = openGraphToObject(geoMeta.openGraph);
163
+ * return {
164
+ * openGraph: {
165
+ * title: ogObj['og:title'],
166
+ * description: ogObj['og:description'],
167
+ * },
168
+ * };
169
+ * ```
170
+ */
171
+ export function openGraphToObject(og: GEOMetadata['openGraph']): Record<string, string> {
172
+ if (!og) return {};
173
+ return Object.fromEntries(og.map(o => [o.property, o.content]));
174
+ }
175
+
176
+ export function generateGEOMetadata(config: GEOConfig): GEOMetadata {
177
+ // Input validation
178
+ if (!config.name || config.name.trim().length === 0) {
179
+ throw new Error('GEOConfig.name is required and cannot be empty');
180
+ }
181
+
182
+ if (!config.description || config.description.trim().length === 0) {
183
+ throw new Error('GEOConfig.description is required and cannot be empty');
184
+ }
185
+
186
+ if (config.description.length > 160) {
187
+ console.warn(
188
+ `[GEO] Description is ${config.description.length} characters. ` +
189
+ `Consider keeping it under 160 for better AI parsing.`
190
+ );
191
+ }
192
+
193
+ if (config.url && !isValidUrl(config.url)) {
194
+ throw new Error(`GEOConfig.url must be a valid URL: ${config.url}`);
195
+ }
196
+
197
+ if (config.codeRepository && !isValidUrl(config.codeRepository)) {
198
+ throw new Error(`GEOConfig.codeRepository must be a valid URL: ${config.codeRepository}`);
199
+ }
200
+
201
+ if (config.documentationUrl && !isValidUrl(config.documentationUrl)) {
202
+ throw new Error(`GEOConfig.documentationUrl must be a valid URL: ${config.documentationUrl}`);
203
+ }
204
+
205
+ // Generate meta tags
206
+ const meta: Array<{ name: string; content: string }> = [
207
+ {
208
+ name: META_NAMES.DESCRIPTION,
209
+ content: config.description,
210
+ },
211
+ ];
212
+
213
+ // Add keywords meta tag (filter empty values)
214
+ const keywordsContent = joinNonEmpty(config.keywords, ', ');
215
+ if (keywordsContent) {
216
+ meta.push({
217
+ name: META_NAMES.KEYWORDS,
218
+ content: keywordsContent,
219
+ });
220
+ }
221
+
222
+ // Add software-specific meta tags
223
+ if (config.version) {
224
+ meta.push({
225
+ name: META_NAMES.SOFTWARE_VERSION,
226
+ content: config.version,
227
+ });
228
+ }
229
+
230
+ const categories = normalizeToArray(config.applicationCategory);
231
+ const categoryContent = joinNonEmpty(categories, ', ');
232
+ if (categoryContent) {
233
+ meta.push({
234
+ name: META_NAMES.SOFTWARE_CATEGORY,
235
+ content: categoryContent,
236
+ });
237
+ }
238
+
239
+ const languages = normalizeToArray(config.programmingLanguage);
240
+ const languageContent = joinNonEmpty(languages, ', ');
241
+ if (languageContent) {
242
+ meta.push({
243
+ name: META_NAMES.SOFTWARE_LANGUAGE,
244
+ content: languageContent,
245
+ });
246
+ }
247
+
248
+ // Generate JSON-LD structured data
249
+ const jsonLd = [generateSoftwareApplicationLD(config)];
250
+
251
+ // Generate Open Graph tags
252
+ const openGraph: Array<{ property: string; content: string }> = [
253
+ {
254
+ property: OG_PROPERTIES.TITLE,
255
+ content: config.name,
256
+ },
257
+ {
258
+ property: OG_PROPERTIES.DESCRIPTION,
259
+ content: config.description,
260
+ },
261
+ {
262
+ property: OG_PROPERTIES.TYPE,
263
+ content: 'website',
264
+ },
265
+ ];
266
+
267
+ if (config.url) {
268
+ openGraph.push({
269
+ property: OG_PROPERTIES.URL,
270
+ content: config.url,
271
+ });
272
+ }
273
+
274
+ // Add Open Graph article tags for software
275
+ if (config.author) {
276
+ openGraph.push({
277
+ property: OG_PROPERTIES.SITE_NAME,
278
+ content: config.author.name,
279
+ });
280
+ }
281
+
282
+ // Generate Twitter Card tags
283
+ const twitter = [
284
+ {
285
+ name: 'twitter:card',
286
+ content: 'summary_large_image',
287
+ },
288
+ {
289
+ name: 'twitter:title',
290
+ content: config.name,
291
+ },
292
+ {
293
+ name: 'twitter:description',
294
+ content: config.description,
295
+ },
296
+ ];
297
+
298
+ return {
299
+ meta,
300
+ jsonLd,
301
+ openGraph,
302
+ twitter,
303
+ version: '1.0.0', // Schema version for future migrations and debugging
304
+ };
305
+ }
306
+
307
+ /**
308
+ * JSON-LD stringification cache
309
+ * 동일한 객체를 여러 번 렌더링할 때 성능 최적화를 위한 캐시
310
+ */
311
+ const stringifiedCache = new WeakMap<object, string>();
312
+
313
+ /**
314
+ * Render JSON-LD for Next.js Script component
315
+ *
316
+ * XSS 보안을 위해 위험한 문자를 이스케이프합니다.
317
+ * Escapes dangerous characters for XSS security.
318
+ *
319
+ * Next.js에서 사용할 수 있는 JSON-LD script 태그 생성
320
+ *
321
+ * @param jsonLd - JSON-LD structured data
322
+ * @param id - Optional script ID (default: auto-generated)
323
+ * @returns Props for Next.js Script component with:
324
+ * - `id`: Unique script ID
325
+ * - `type`: 'application/ld+json'
326
+ * - `dangerouslySetInnerHTML.__html`: Escaped JSON string
327
+ *
328
+ * @example
329
+ * ```tsx
330
+ * import Script from 'next/script';
331
+ * import { renderJSONLD } from '@hua-labs/hua-ux/framework';
332
+ *
333
+ * const geoMeta = generateGEOMetadata({ ... });
334
+ *
335
+ * export default function Page() {
336
+ * return (
337
+ * <>
338
+ * <Script {...renderJSONLD(geoMeta.jsonLd[0])} />
339
+ * <main>...</main>
340
+ * </>
341
+ * );
342
+ * }
343
+ * ```
344
+ */
345
+ export function renderJSONLD(jsonLd: unknown, id?: string): {
346
+ id: string;
347
+ type: string;
348
+ dangerouslySetInnerHTML: { __html: string };
349
+ } {
350
+ let jsonString: string;
351
+
352
+ // 캐시된 문자열이 있으면 재사용 (성능 최적화)
353
+ // Reuse cached string if available (performance optimization)
354
+ if (typeof jsonLd === 'object' && jsonLd !== null) {
355
+ if (stringifiedCache.has(jsonLd)) {
356
+ jsonString = stringifiedCache.get(jsonLd)!;
357
+ } else {
358
+ // XSS 보안: </script> 태그가 JSON 문자열에 포함되어도 안전하게 이스케이프
359
+ // XSS Security: Escape dangerous characters even if </script> appears in JSON string
360
+ jsonString = JSON.stringify(jsonLd)
361
+ .replace(/</g, '\\u003c')
362
+ .replace(/>/g, '\\u003e')
363
+ .replace(/&/g, '\\u0026');
364
+ stringifiedCache.set(jsonLd, jsonString);
365
+ }
366
+ } else {
367
+ // 원시 타입은 캐싱하지 않음
368
+ jsonString = String(jsonLd);
369
+ }
370
+
371
+ return {
372
+ id: id || `jsonld-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
373
+ type: 'application/ld+json',
374
+ dangerouslySetInnerHTML: {
375
+ __html: jsonString,
376
+ },
377
+ };
378
+ }
379
+
380
+ /**
381
+ * Create AI-friendly context description
382
+ *
383
+ * AI가 맥락을 이해하기 쉽도록 풍부한 설명 생성
384
+ *
385
+ * @param config - GEO configuration
386
+ * @returns AI-friendly context string
387
+ *
388
+ * @example
389
+ * ```tsx
390
+ * const context = createAIContext({
391
+ * name: 'hua-ux',
392
+ * description: 'Privacy-first UX framework',
393
+ * features: ['i18n', 'Motion', 'Accessibility'],
394
+ * useCases: ['Multilingual apps', 'Accessible UX'],
395
+ * });
396
+ * // Returns: "hua-ux is a Privacy-first UX framework. Key features include: i18n, Motion, Accessibility. Common use cases: Multilingual apps, Accessible UX."
397
+ * ```
398
+ */
399
+ export function createAIContext(config: GEOConfig): string {
400
+ const parts: string[] = [];
401
+
402
+ // Basic description
403
+ parts.push(`${config.name} is a ${config.description}`);
404
+
405
+ // Features (filter empty values)
406
+ const featureContent = joinNonEmpty(config.features, ', ');
407
+ if (featureContent) {
408
+ parts.push(`Key features include: ${featureContent}`);
409
+ }
410
+
411
+ // Use cases (filter empty values)
412
+ const useCaseContent = joinNonEmpty(config.useCases, ', ');
413
+ if (useCaseContent) {
414
+ parts.push(`Common use cases: ${useCaseContent}`);
415
+ }
416
+
417
+ // Programming language
418
+ const languages = normalizeToArray(config.programmingLanguage);
419
+ const languageContent = joinNonEmpty(languages, ', ');
420
+ if (languageContent) {
421
+ parts.push(`Built with: ${languageContent}`);
422
+ }
423
+
424
+ // Technology stack
425
+ const techStack = normalizeToArray(config.technologyStack);
426
+ const techStackContent = joinNonEmpty(techStack, ', ');
427
+ if (techStackContent) {
428
+ parts.push(`Technology stack: ${techStackContent}`);
429
+ }
430
+
431
+ // Requirements (filter empty values)
432
+ const requirementsContent = joinNonEmpty(config.softwareRequirements, ', ');
433
+ if (requirementsContent) {
434
+ parts.push(`Requires: ${requirementsContent}`);
435
+ }
436
+
437
+ const result = parts.join('. ');
438
+ // 중복 마침표 방지 (이미 마침표로 끝나면 추가하지 않음)
439
+ // Prevent double periods (don't add if already ends with period)
440
+ return result.endsWith('.') ? result : result + '.';
441
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @hua-labs/hua-ux/framework - GEO (Generative Engine Optimization)
3
+ *
4
+ * Make your application discoverable and recommendable by AI search engines
5
+ * (ChatGPT, Claude, Gemini, Perplexity)
6
+ *
7
+ * ChatGPT, Claude, Gemini, Perplexity 같은 AI 검색 엔진이
8
+ * 당신의 애플리케이션을 잘 찾고 추천하도록 최적화
9
+ */
10
+
11
+ // Main GEO function
12
+ export {
13
+ generateGEOMetadata,
14
+ renderJSONLD,
15
+ createAIContext,
16
+ metaToObject,
17
+ openGraphToObject,
18
+ } from './generateGEOMetadata';
19
+
20
+ // Structured data helpers
21
+ export {
22
+ generateSoftwareApplicationLD,
23
+ generateFAQPageLD,
24
+ generateTechArticleLD,
25
+ generateHowToLD,
26
+ generateCodeLD,
27
+ generateVideoLD,
28
+ generateOrganizationLD,
29
+ } from './structuredData';
30
+
31
+ // Presets
32
+ export { GEO_PRESETS } from './presets';
33
+ export type { GEOPreset } from './presets';
34
+
35
+ // Test utilities
36
+ export {
37
+ validateGEOMetadata,
38
+ prettyPrintGEOMetadata,
39
+ compareGEOMetadata,
40
+ } from './test-utils';
41
+ export type { GEOValidationResult } from './test-utils';
42
+
43
+ // Constants
44
+ export { META_NAMES, OG_PROPERTIES } from './types';
45
+
46
+ // Types
47
+ export type {
48
+ GEOConfig,
49
+ GEOMetadata,
50
+ StructuredData,
51
+ SoftwareApplicationType,
52
+ SoftwareCategory,
53
+ ProgrammingLanguage,
54
+ TechnologyStack,
55
+ RequiredGEOConfig,
56
+ OptionalGEOConfig,
57
+ GEOConfigInput,
58
+ } from './types';
59
+
60
+ // Type guards
61
+ export { isValidGEOConfig } from './types';
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @hua-labs/hua-ux/framework - GEO Presets
3
+ *
4
+ * Pre-configured GEO presets for common use cases
5
+ * 일반적인 사용 사례를 위한 사전 구성된 GEO 프리셋
6
+ */
7
+
8
+ import type { GEOConfig } from './types';
9
+
10
+ /**
11
+ * GEO Presets
12
+ * 일반적인 소프트웨어 타입을 위한 사전 구성된 설정
13
+ */
14
+ export const GEO_PRESETS = {
15
+ /**
16
+ * Next.js Framework preset
17
+ * Next.js 프레임워크용 프리셋
18
+ */
19
+ NEXTJS_FRAMEWORK: {
20
+ applicationType: 'DeveloperApplication' as const,
21
+ programmingLanguage: ['TypeScript'] as const,
22
+ technologyStack: ['Next.js', 'React'] as const,
23
+ applicationCategory: 'Developer Tool' as const,
24
+ },
25
+
26
+ /**
27
+ * UI Library preset
28
+ * UI 라이브러리용 프리셋
29
+ */
30
+ UI_LIBRARY: {
31
+ applicationType: 'DeveloperApplication' as const,
32
+ applicationCategory: 'Component Library' as const,
33
+ },
34
+
35
+ /**
36
+ * React Application preset
37
+ * React 애플리케이션용 프리셋
38
+ */
39
+ REACT_APP: {
40
+ applicationType: 'WebApplication' as const,
41
+ programmingLanguage: ['TypeScript', 'JavaScript'] as const,
42
+ technologyStack: ['React'] as const,
43
+ },
44
+
45
+ /**
46
+ * NPM Package preset
47
+ * NPM 패키지용 프리셋
48
+ */
49
+ NPM_PACKAGE: {
50
+ applicationType: 'DeveloperApplication' as const,
51
+ applicationCategory: 'Developer Tool' as const,
52
+ },
53
+ } as const satisfies Record<string, Partial<GEOConfig>>;
54
+
55
+ /**
56
+ * Type helper for preset values
57
+ */
58
+ export type GEOPreset = typeof GEO_PRESETS[keyof typeof GEO_PRESETS];