@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,422 @@
1
+ /**
2
+ * @hua-labs/hua-ux/framework - Structured Data Helpers
3
+ *
4
+ * Schema.org JSON-LD helpers for AI search engines
5
+ * AI 검색 엔진이 이해하기 쉬운 구조화된 데이터 생성
6
+ */
7
+
8
+ import type {
9
+ GEOConfig,
10
+ StructuredData,
11
+ SoftwareApplicationType,
12
+ ProgrammingLanguage,
13
+ SoftwareCategory,
14
+ TechnologyStack,
15
+ } from './types';
16
+
17
+ /**
18
+ * Normalize array (single value or array to array)
19
+ */
20
+ function normalizeToArray<T>(value: T | T[] | undefined): T[] {
21
+ if (value === undefined) return [];
22
+ return Array.isArray(value) ? value : [value];
23
+ }
24
+
25
+ /**
26
+ * Join non-empty array values
27
+ */
28
+ function joinNonEmpty(values: string[] | undefined, separator: string): string | undefined {
29
+ if (!values || values.length === 0) return undefined;
30
+ const nonEmpty = values.filter(v => v && v.trim().length > 0);
31
+ return nonEmpty.length > 0 ? nonEmpty.join(separator) : undefined;
32
+ }
33
+
34
+ /**
35
+ * Generate Schema.org SoftwareApplication JSON-LD
36
+ *
37
+ * AI 검색 엔진이 소프트웨어를 정확하게 이해하도록 Schema.org 구조화된 데이터 생성
38
+ *
39
+ * @param config - GEO configuration
40
+ * @returns Schema.org JSON-LD structured data
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * const jsonLd = generateSoftwareApplicationLD({
45
+ * name: 'hua-ux',
46
+ * description: 'Privacy-first UX framework for Next.js',
47
+ * version: '1.0.0',
48
+ * applicationCategory: 'UX Framework',
49
+ * programmingLanguage: ['TypeScript', 'React'],
50
+ * features: ['i18n', 'Motion', 'Accessibility'],
51
+ * });
52
+ * ```
53
+ */
54
+ export function generateSoftwareApplicationLD(config: GEOConfig): StructuredData {
55
+ const jsonLd: StructuredData = {
56
+ '@context': 'https://schema.org',
57
+ '@type': 'SoftwareApplication',
58
+ name: config.name,
59
+ description: config.description,
60
+ };
61
+
62
+ // Alternative names (filter empty values)
63
+ const alternateNames = normalizeToArray(config.alternateName);
64
+ const validAlternateNames = alternateNames.filter(name => name && name.trim().length > 0);
65
+ if (validAlternateNames.length > 0) {
66
+ jsonLd.alternateName = validAlternateNames;
67
+ }
68
+
69
+ // Version
70
+ if (config.version) {
71
+ jsonLd.softwareVersion = config.version;
72
+ }
73
+
74
+ // Application category (filter empty values)
75
+ const categories = normalizeToArray(config.applicationCategory);
76
+ const categoryContent = joinNonEmpty(categories, ', ');
77
+ if (categoryContent) {
78
+ jsonLd.applicationCategory = categoryContent;
79
+ }
80
+
81
+ // Programming language (filter empty values)
82
+ const languages = normalizeToArray(config.programmingLanguage);
83
+ const languageContent = joinNonEmpty(languages, ', ');
84
+ if (languageContent) {
85
+ jsonLd.programmingLanguage = languageContent;
86
+ }
87
+
88
+ // Technology stack (filter empty values)
89
+ // Note: Schema.org doesn't have a direct "technologyStack" field,
90
+ // so we include it in keywords for AI discoverability
91
+ const techStack = normalizeToArray(config.technologyStack);
92
+ const techStackContent = joinNonEmpty(techStack, ', ');
93
+ if (techStackContent && jsonLd.keywords) {
94
+ // Append to existing keywords
95
+ jsonLd.keywords = `${jsonLd.keywords}, ${techStackContent}`;
96
+ } else if (techStackContent) {
97
+ // Create keywords if doesn't exist
98
+ jsonLd.keywords = techStackContent;
99
+ }
100
+
101
+ // Application type
102
+ if (config.applicationType) {
103
+ jsonLd.applicationSubType = config.applicationType;
104
+ }
105
+
106
+ // URLs
107
+ if (config.url) {
108
+ jsonLd.url = config.url;
109
+ }
110
+
111
+ if (config.documentationUrl) {
112
+ jsonLd.softwareHelp = {
113
+ '@type': 'CreativeWork',
114
+ url: config.documentationUrl,
115
+ };
116
+ }
117
+
118
+ if (config.codeRepository) {
119
+ jsonLd.codeRepository = config.codeRepository;
120
+ }
121
+
122
+ // License
123
+ if (config.license) {
124
+ jsonLd.license = config.license;
125
+ }
126
+
127
+ // Author
128
+ if (config.author) {
129
+ jsonLd.author = {
130
+ '@type': 'Organization',
131
+ name: config.author.name,
132
+ ...(config.author.url && { url: config.author.url }),
133
+ };
134
+ }
135
+
136
+ // Features as keywords (filter empty values)
137
+ const featureList = joinNonEmpty(config.features, ', ');
138
+ if (featureList) {
139
+ jsonLd.featureList = featureList;
140
+ }
141
+
142
+ // Keywords (filter empty values)
143
+ const keywordsContent = joinNonEmpty(config.keywords, ', ');
144
+ if (keywordsContent) {
145
+ jsonLd.keywords = keywordsContent;
146
+ }
147
+
148
+ // Operating system (filter empty values)
149
+ const osContent = joinNonEmpty(config.operatingSystem, ', ');
150
+ if (osContent) {
151
+ jsonLd.operatingSystem = osContent;
152
+ }
153
+
154
+ // Software requirements (filter empty values)
155
+ const requirementsContent = joinNonEmpty(config.softwareRequirements, ', ');
156
+ if (requirementsContent) {
157
+ jsonLd.softwareRequirements = requirementsContent;
158
+ }
159
+
160
+ return jsonLd;
161
+ }
162
+
163
+ /**
164
+ * Generate Schema.org Code JSON-LD
165
+ *
166
+ * AI가 코드 스니펫과 예제를 이해하도록 Code 구조화된 데이터 생성
167
+ *
168
+ * @param code - Code configuration
169
+ * @returns Schema.org Code JSON-LD
170
+ *
171
+ * @example
172
+ * ```tsx
173
+ * const codeLd = generateCodeLD({
174
+ * programmingLanguage: 'TypeScript',
175
+ * text: 'const x = 1;',
176
+ * name: 'Example Code',
177
+ * });
178
+ * ```
179
+ */
180
+ export function generateCodeLD(code: {
181
+ programmingLanguage: string;
182
+ text: string;
183
+ name?: string;
184
+ }): StructuredData {
185
+ return {
186
+ '@context': 'https://schema.org',
187
+ '@type': 'Code',
188
+ programmingLanguage: code.programmingLanguage,
189
+ text: code.text,
190
+ ...(code.name && { name: code.name }),
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Generate Schema.org VideoObject JSON-LD
196
+ *
197
+ * AI가 튜토리얼 비디오를 이해하고 추천할 수 있도록 VideoObject 구조화된 데이터 생성
198
+ *
199
+ * @param video - Video configuration
200
+ * @returns Schema.org VideoObject JSON-LD
201
+ *
202
+ * @example
203
+ * ```tsx
204
+ * const videoLd = generateVideoLD({
205
+ * name: 'Getting Started with hua-ux',
206
+ * description: 'Learn how to build with hua-ux',
207
+ * thumbnailUrl: 'https://example.com/thumb.jpg',
208
+ * uploadDate: '2025-12-29',
209
+ * duration: 'PT10M30S',
210
+ * });
211
+ * ```
212
+ */
213
+ export function generateVideoLD(video: {
214
+ name: string;
215
+ description: string;
216
+ thumbnailUrl: string;
217
+ uploadDate: string;
218
+ duration?: string;
219
+ contentUrl?: string;
220
+ }): StructuredData {
221
+ return {
222
+ '@context': 'https://schema.org',
223
+ '@type': 'VideoObject',
224
+ name: video.name,
225
+ description: video.description,
226
+ thumbnailUrl: video.thumbnailUrl,
227
+ uploadDate: video.uploadDate,
228
+ ...(video.duration && { duration: video.duration }),
229
+ ...(video.contentUrl && { contentUrl: video.contentUrl }),
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Generate Schema.org Organization JSON-LD
235
+ *
236
+ * AI가 조직/회사 정보를 이해하도록 Organization 구조화된 데이터 생성
237
+ *
238
+ * @param org - Organization configuration
239
+ * @returns Schema.org Organization JSON-LD
240
+ *
241
+ * @example
242
+ * ```tsx
243
+ * const orgLd = generateOrganizationLD({
244
+ * name: 'hua-labs',
245
+ * url: 'https://hua-labs.dev',
246
+ * logo: 'https://hua-labs.dev/logo.png',
247
+ * description: 'Privacy-first development tools',
248
+ * });
249
+ * ```
250
+ */
251
+ export function generateOrganizationLD(org: {
252
+ name: string;
253
+ url?: string;
254
+ logo?: string;
255
+ description?: string;
256
+ }): StructuredData {
257
+ return {
258
+ '@context': 'https://schema.org',
259
+ '@type': 'Organization',
260
+ name: org.name,
261
+ ...(org.url && { url: org.url }),
262
+ ...(org.logo && { logo: org.logo }),
263
+ ...(org.description && { description: org.description }),
264
+ };
265
+ }
266
+
267
+ /**
268
+ * Generate Schema.org FAQPage JSON-LD
269
+ *
270
+ * AI가 자주 묻는 질문에 답변할 수 있도록 FAQ 구조화된 데이터 생성
271
+ *
272
+ * @param faqs - Array of FAQ items
273
+ * @returns Schema.org FAQ JSON-LD
274
+ *
275
+ * @example
276
+ * ```tsx
277
+ * const faqLd = generateFAQPageLD([
278
+ * {
279
+ * question: 'What is hua-ux?',
280
+ * answer: 'hua-ux is a privacy-first UX framework for Next.js applications.',
281
+ * },
282
+ * {
283
+ * question: 'How do I install hua-ux?',
284
+ * answer: 'Run: npx @hua-labs/create-hua-ux my-app',
285
+ * },
286
+ * ]);
287
+ * ```
288
+ */
289
+ export function generateFAQPageLD(
290
+ faqs: Array<{ question: string; answer: string }>
291
+ ): StructuredData {
292
+ // Input validation
293
+ if (!faqs || faqs.length === 0) {
294
+ throw new Error('FAQPage requires at least one FAQ item');
295
+ }
296
+
297
+ // Filter and validate FAQ items
298
+ const validFaqs = faqs.filter(faq => {
299
+ return faq.question?.trim() && faq.answer?.trim();
300
+ });
301
+
302
+ if (validFaqs.length === 0) {
303
+ throw new Error('All FAQ items have empty questions or answers');
304
+ }
305
+
306
+ return {
307
+ '@context': 'https://schema.org',
308
+ '@type': 'FAQPage',
309
+ mainEntity: validFaqs.map((faq) => ({
310
+ '@type': 'Question',
311
+ name: faq.question.trim(),
312
+ acceptedAnswer: {
313
+ '@type': 'Answer',
314
+ text: faq.answer.trim(),
315
+ },
316
+ })),
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Generate Schema.org TechArticle JSON-LD
322
+ *
323
+ * AI가 기술 문서를 정확하게 이해하도록 기술 아티클 구조화된 데이터 생성
324
+ *
325
+ * @param article - Article configuration
326
+ * @returns Schema.org TechArticle JSON-LD
327
+ *
328
+ * @example
329
+ * ```tsx
330
+ * const articleLd = generateTechArticleLD({
331
+ * headline: 'Getting Started with hua-ux',
332
+ * description: 'Learn how to build privacy-first UX with hua-ux',
333
+ * datePublished: '2025-12-29',
334
+ * author: { name: 'hua-labs' },
335
+ * });
336
+ * ```
337
+ */
338
+ export function generateTechArticleLD(article: {
339
+ headline: string;
340
+ description?: string;
341
+ datePublished?: string;
342
+ dateModified?: string;
343
+ author?: { name: string; url?: string };
344
+ image?: string;
345
+ }): StructuredData {
346
+ return {
347
+ '@context': 'https://schema.org',
348
+ '@type': 'TechArticle',
349
+ headline: article.headline,
350
+ ...(article.description && { description: article.description }),
351
+ ...(article.datePublished && { datePublished: article.datePublished }),
352
+ ...(article.dateModified && { dateModified: article.dateModified }),
353
+ ...(article.author && {
354
+ author: {
355
+ '@type': 'Organization',
356
+ name: article.author.name,
357
+ ...(article.author.url && { url: article.author.url }),
358
+ },
359
+ }),
360
+ ...(article.image && { image: article.image }),
361
+ };
362
+ }
363
+
364
+ /**
365
+ * Generate Schema.org HowTo JSON-LD
366
+ *
367
+ * AI가 튜토리얼/가이드를 이해하고 추천할 수 있도록 HowTo 구조화된 데이터 생성
368
+ *
369
+ * @param howTo - HowTo configuration
370
+ * @returns Schema.org HowTo JSON-LD
371
+ *
372
+ * @example
373
+ * ```tsx
374
+ * const howToLd = generateHowToLD({
375
+ * name: 'How to add i18n to your Next.js app',
376
+ * description: 'Step-by-step guide to internationalization',
377
+ * steps: [
378
+ * { name: 'Install hua-ux', text: 'Run: npx @hua-labs/create-hua-ux my-app' },
379
+ * { name: 'Configure i18n', text: 'Add locales to your config' },
380
+ * ],
381
+ * });
382
+ * ```
383
+ */
384
+ export function generateHowToLD(howTo: {
385
+ name: string;
386
+ description?: string;
387
+ steps: Array<{ name: string; text: string; image?: string }>;
388
+ totalTime?: string;
389
+ }): StructuredData {
390
+ // Input validation
391
+ if (!howTo.name || howTo.name.trim().length === 0) {
392
+ throw new Error('HowTo.name is required and cannot be empty');
393
+ }
394
+
395
+ if (!howTo.steps || howTo.steps.length === 0) {
396
+ throw new Error('HowTo requires at least one step');
397
+ }
398
+
399
+ // Filter and validate steps
400
+ const validSteps = howTo.steps
401
+ .filter(step => step.name?.trim() && step.text?.trim())
402
+ .map((step, index) => ({
403
+ '@type': 'HowToStep' as const,
404
+ position: index + 1,
405
+ name: step.name.trim(),
406
+ text: step.text.trim(),
407
+ ...(step.image && { image: step.image }),
408
+ }));
409
+
410
+ if (validSteps.length === 0) {
411
+ throw new Error('All HowTo steps have empty names or text');
412
+ }
413
+
414
+ return {
415
+ '@context': 'https://schema.org',
416
+ '@type': 'HowTo',
417
+ name: howTo.name.trim(),
418
+ step: validSteps,
419
+ ...(howTo.description && { description: howTo.description }),
420
+ ...(howTo.totalTime && { totalTime: howTo.totalTime }),
421
+ };
422
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * @hua-labs/hua-ux/framework - GEO Test Utilities
3
+ *
4
+ * Testing utilities for GEO metadata validation and debugging
5
+ * GEO 메타데이터 검증 및 디버깅을 위한 테스트 유틸리티
6
+ */
7
+
8
+ import type { GEOMetadata } from './types';
9
+
10
+ /**
11
+ * Validation result
12
+ * 검증 결과
13
+ */
14
+ export interface GEOValidationResult {
15
+ /**
16
+ * Whether the metadata is valid
17
+ */
18
+ valid: boolean;
19
+
20
+ /**
21
+ * Array of error messages
22
+ */
23
+ errors: string[];
24
+
25
+ /**
26
+ * Array of warning messages
27
+ */
28
+ warnings: string[];
29
+ }
30
+
31
+ /**
32
+ * Validate GEO metadata
33
+ *
34
+ * GEO 메타데이터의 유효성을 검증합니다.
35
+ *
36
+ * @param metadata - GEO metadata to validate
37
+ * @returns Validation result with errors and warnings
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * const result = validateGEOMetadata(geoMeta);
42
+ * if (!result.valid) {
43
+ * console.error('Validation errors:', result.errors);
44
+ * }
45
+ * ```
46
+ */
47
+ export function validateGEOMetadata(metadata: GEOMetadata): GEOValidationResult {
48
+ const errors: string[] = [];
49
+ const warnings: string[] = [];
50
+
51
+ // Validate meta tags
52
+ const descMeta = metadata.meta.find(m => m.name === 'description');
53
+ if (!descMeta) {
54
+ errors.push('Missing description meta tag');
55
+ } else if (descMeta.content.length === 0) {
56
+ errors.push('Description meta tag is empty');
57
+ } else if (descMeta.content.length > 160) {
58
+ warnings.push(
59
+ `Description exceeds 160 characters (${descMeta.content.length}). ` +
60
+ 'Consider keeping it under 160 for better AI parsing.'
61
+ );
62
+ }
63
+
64
+ // Validate JSON-LD
65
+ if (!metadata.jsonLd || metadata.jsonLd.length === 0) {
66
+ errors.push('Missing JSON-LD structured data');
67
+ } else {
68
+ for (const ld of metadata.jsonLd) {
69
+ if (!ld['@context'] || ld['@context'] !== 'https://schema.org') {
70
+ errors.push('Invalid JSON-LD structure: missing or invalid @context');
71
+ }
72
+ if (!ld['@type']) {
73
+ errors.push('Invalid JSON-LD structure: missing @type');
74
+ }
75
+ }
76
+ }
77
+
78
+ // Validate URLs in meta tags
79
+ metadata.meta.forEach(meta => {
80
+ if (meta.content.startsWith('http')) {
81
+ try {
82
+ new URL(meta.content);
83
+ } catch {
84
+ errors.push(`Invalid URL in meta tag ${meta.name}: ${meta.content}`);
85
+ }
86
+ }
87
+ });
88
+
89
+ // Validate Open Graph tags
90
+ if (metadata.openGraph) {
91
+ const requiredOG = ['og:title', 'og:description', 'og:type'];
92
+ const ogProperties = metadata.openGraph.map(og => og.property);
93
+ for (const required of requiredOG) {
94
+ if (!ogProperties.includes(required)) {
95
+ warnings.push(`Missing recommended Open Graph property: ${required}`);
96
+ }
97
+ }
98
+ }
99
+
100
+ return {
101
+ valid: errors.length === 0,
102
+ errors,
103
+ warnings,
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Pretty print GEO metadata
109
+ *
110
+ * GEO 메타데이터를 읽기 쉬운 형식으로 출력합니다.
111
+ *
112
+ * @param metadata - GEO metadata to print
113
+ * @returns Formatted JSON string
114
+ *
115
+ * @example
116
+ * ```tsx
117
+ * console.log(prettyPrintGEOMetadata(geoMeta));
118
+ * ```
119
+ */
120
+ export function prettyPrintGEOMetadata(metadata: GEOMetadata): string {
121
+ return JSON.stringify(metadata, null, 2);
122
+ }
123
+
124
+ /**
125
+ * Compare two GEO metadata objects
126
+ *
127
+ * 두 GEO 메타데이터 객체를 비교합니다.
128
+ *
129
+ * @param a - First GEO metadata
130
+ * @param b - Second GEO metadata
131
+ * @returns Comparison result with differences
132
+ *
133
+ * @example
134
+ * ```tsx
135
+ * const result = compareGEOMetadata(meta1, meta2);
136
+ * if (!result.same) {
137
+ * console.log('Differences:', result.differences);
138
+ * }
139
+ * ```
140
+ */
141
+ export function compareGEOMetadata(
142
+ a: GEOMetadata,
143
+ b: GEOMetadata
144
+ ): { same: boolean; differences: string[] } {
145
+ const differences: string[] = [];
146
+
147
+ // Compare meta tags
148
+ const aMetaMap = new Map(a.meta.map(m => [m.name, m.content]));
149
+ const bMetaMap = new Map(b.meta.map(m => [m.name, m.content]));
150
+
151
+ for (const [name, content] of aMetaMap) {
152
+ if (bMetaMap.get(name) !== content) {
153
+ differences.push(`Meta tag '${name}' differs`);
154
+ }
155
+ }
156
+
157
+ for (const [name] of bMetaMap) {
158
+ if (!aMetaMap.has(name)) {
159
+ differences.push(`Meta tag '${name}' missing in first metadata`);
160
+ }
161
+ }
162
+
163
+ // Compare JSON-LD count
164
+ if (a.jsonLd.length !== b.jsonLd.length) {
165
+ differences.push(
166
+ `JSON-LD count differs: ${a.jsonLd.length} vs ${b.jsonLd.length}`
167
+ );
168
+ }
169
+
170
+ // Compare version
171
+ if (a.version !== b.version) {
172
+ differences.push(`Version differs: ${a.version} vs ${b.version}`);
173
+ }
174
+
175
+ return {
176
+ same: differences.length === 0,
177
+ differences,
178
+ };
179
+ }