@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.
- package/README.md +839 -0
- package/dist/framework/a11y/components/LiveRegion.d.ts +64 -0
- package/dist/framework/a11y/components/LiveRegion.d.ts.map +1 -0
- package/dist/framework/a11y/components/LiveRegion.js +43 -0
- package/dist/framework/a11y/components/SkipToContent.d.ts +62 -0
- package/dist/framework/a11y/components/SkipToContent.d.ts.map +1 -0
- package/dist/framework/a11y/components/SkipToContent.js +60 -0
- package/dist/framework/a11y/hooks/useFocusManagement.d.ts +60 -0
- package/dist/framework/a11y/hooks/useFocusManagement.d.ts.map +1 -0
- package/dist/framework/a11y/hooks/useFocusManagement.js +71 -0
- package/dist/framework/a11y/hooks/useFocusTrap.d.ts +64 -0
- package/dist/framework/a11y/hooks/useFocusTrap.d.ts.map +1 -0
- package/dist/framework/a11y/hooks/useFocusTrap.js +185 -0
- package/dist/framework/a11y/hooks/useLiveRegion.d.ts +56 -0
- package/dist/framework/a11y/hooks/useLiveRegion.d.ts.map +1 -0
- package/dist/framework/a11y/hooks/useLiveRegion.js +60 -0
- package/dist/framework/a11y/index.d.ts +16 -0
- package/dist/framework/a11y/index.d.ts.map +1 -0
- package/dist/framework/a11y/index.js +11 -0
- package/dist/framework/branding/context.d.ts +52 -0
- package/dist/framework/branding/context.d.ts.map +1 -0
- package/dist/framework/branding/context.js +96 -0
- package/dist/framework/branding/css-vars.d.ts +34 -0
- package/dist/framework/branding/css-vars.d.ts.map +1 -0
- package/dist/framework/branding/css-vars.js +95 -0
- package/dist/framework/branding/tailwind-config.d.ts +38 -0
- package/dist/framework/branding/tailwind-config.d.ts.map +1 -0
- package/dist/framework/branding/tailwind-config.js +66 -0
- package/dist/framework/components/BrandedButton.d.ts +53 -0
- package/dist/framework/components/BrandedButton.d.ts.map +1 -0
- package/dist/framework/components/BrandedButton.js +40 -0
- package/dist/framework/components/BrandedCard.d.ts +52 -0
- package/dist/framework/components/BrandedCard.d.ts.map +1 -0
- package/dist/framework/components/BrandedCard.js +73 -0
- package/dist/framework/components/ErrorBoundary.d.ts +92 -0
- package/dist/framework/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/framework/components/ErrorBoundary.js +121 -0
- package/dist/framework/components/HuaUxLayout.d.ts +29 -0
- package/dist/framework/components/HuaUxLayout.d.ts.map +1 -0
- package/dist/framework/components/HuaUxLayout.js +32 -0
- package/dist/framework/components/HuaUxPage.d.ts +48 -0
- package/dist/framework/components/HuaUxPage.d.ts.map +1 -0
- package/dist/framework/components/HuaUxPage.js +105 -0
- package/dist/framework/components/Providers.d.ts +17 -0
- package/dist/framework/components/Providers.d.ts.map +1 -0
- package/dist/framework/components/Providers.js +72 -0
- package/dist/framework/components/WelcomePage.d.ts +44 -0
- package/dist/framework/components/WelcomePage.d.ts.map +1 -0
- package/dist/framework/components/WelcomePage.js +80 -0
- package/dist/framework/config/index.d.ts +182 -0
- package/dist/framework/config/index.d.ts.map +1 -0
- package/dist/framework/config/index.js +329 -0
- package/dist/framework/config/merge.d.ts +26 -0
- package/dist/framework/config/merge.d.ts.map +1 -0
- package/dist/framework/config/merge.js +160 -0
- package/dist/framework/config/schema.d.ts +25 -0
- package/dist/framework/config/schema.d.ts.map +1 -0
- package/dist/framework/config/schema.js +122 -0
- package/dist/framework/hooks/useMotion.d.ts +45 -0
- package/dist/framework/hooks/useMotion.d.ts.map +1 -0
- package/dist/framework/hooks/useMotion.js +40 -0
- package/dist/framework/index.d.ts +37 -0
- package/dist/framework/index.d.ts.map +1 -0
- package/dist/framework/index.js +42 -0
- package/dist/framework/license/errors.d.ts +15 -0
- package/dist/framework/license/errors.d.ts.map +1 -0
- package/dist/framework/license/errors.js +52 -0
- package/dist/framework/license/index.d.ts +70 -0
- package/dist/framework/license/index.d.ts.map +1 -0
- package/dist/framework/license/index.js +124 -0
- package/dist/framework/license/loader.d.ts +26 -0
- package/dist/framework/license/loader.d.ts.map +1 -0
- package/dist/framework/license/loader.js +137 -0
- package/dist/framework/license/types.d.ts +67 -0
- package/dist/framework/license/types.d.ts.map +1 -0
- package/dist/framework/license/types.js +18 -0
- package/dist/framework/loading/components/SkeletonGroup.d.ts +44 -0
- package/dist/framework/loading/components/SkeletonGroup.d.ts.map +1 -0
- package/dist/framework/loading/components/SkeletonGroup.js +34 -0
- package/dist/framework/loading/components/SuspenseWrapper.d.ts +58 -0
- package/dist/framework/loading/components/SuspenseWrapper.d.ts.map +1 -0
- package/dist/framework/loading/components/SuspenseWrapper.js +40 -0
- package/dist/framework/loading/hoc/withSuspense.d.ts +46 -0
- package/dist/framework/loading/hoc/withSuspense.d.ts.map +1 -0
- package/dist/framework/loading/hoc/withSuspense.js +54 -0
- package/dist/framework/loading/hooks/useDelayedLoading.d.ts +56 -0
- package/dist/framework/loading/hooks/useDelayedLoading.d.ts.map +1 -0
- package/dist/framework/loading/hooks/useDelayedLoading.js +97 -0
- package/dist/framework/loading/hooks/useLoadingState.d.ts +69 -0
- package/dist/framework/loading/hooks/useLoadingState.d.ts.map +1 -0
- package/dist/framework/loading/hooks/useLoadingState.js +59 -0
- package/dist/framework/loading/index.d.ts +16 -0
- package/dist/framework/loading/index.d.ts.map +1 -0
- package/dist/framework/loading/index.js +13 -0
- package/dist/framework/middleware/i18n.d.ts +90 -0
- package/dist/framework/middleware/i18n.d.ts.map +1 -0
- package/dist/framework/middleware/i18n.js +99 -0
- package/dist/framework/plugins/index.d.ts +8 -0
- package/dist/framework/plugins/index.d.ts.map +1 -0
- package/dist/framework/plugins/index.js +6 -0
- package/dist/framework/plugins/registry.d.ts +95 -0
- package/dist/framework/plugins/registry.d.ts.map +1 -0
- package/dist/framework/plugins/registry.js +160 -0
- package/dist/framework/plugins/types.d.ts +97 -0
- package/dist/framework/plugins/types.d.ts.map +1 -0
- package/dist/framework/plugins/types.js +6 -0
- package/dist/framework/seo/geo/examples.d.ts +87 -0
- package/dist/framework/seo/geo/examples.d.ts.map +1 -0
- package/dist/framework/seo/geo/examples.js +295 -0
- package/dist/framework/seo/geo/generateGEOMetadata.d.ts +107 -0
- package/dist/framework/seo/geo/generateGEOMetadata.d.ts.map +1 -0
- package/dist/framework/seo/geo/generateGEOMetadata.js +404 -0
- package/dist/framework/seo/geo/index.d.ts +19 -0
- package/dist/framework/seo/geo/index.d.ts.map +1 -0
- package/dist/framework/seo/geo/index.js +21 -0
- package/dist/framework/seo/geo/presets.d.ts +52 -0
- package/dist/framework/seo/geo/presets.d.ts.map +1 -0
- package/dist/framework/seo/geo/presets.js +47 -0
- package/dist/framework/seo/geo/structuredData.d.ts +187 -0
- package/dist/framework/seo/geo/structuredData.d.ts.map +1 -0
- package/dist/framework/seo/geo/structuredData.js +354 -0
- package/dist/framework/seo/geo/test-utils.d.ts +78 -0
- package/dist/framework/seo/geo/test-utils.d.ts.map +1 -0
- package/dist/framework/seo/geo/test-utils.js +139 -0
- package/dist/framework/seo/geo/types.d.ts +225 -0
- package/dist/framework/seo/geo/types.d.ts.map +1 -0
- package/dist/framework/seo/geo/types.js +51 -0
- package/dist/framework/types/index.d.ts +577 -0
- package/dist/framework/types/index.d.ts.map +1 -0
- package/dist/framework/types/index.js +6 -0
- package/dist/framework/utils/data-fetching.d.ts +45 -0
- package/dist/framework/utils/data-fetching.d.ts.map +1 -0
- package/dist/framework/utils/data-fetching.js +74 -0
- package/dist/framework/utils/file-structure.d.ts +29 -0
- package/dist/framework/utils/file-structure.d.ts.map +1 -0
- package/dist/framework/utils/file-structure.js +72 -0
- package/dist/framework/utils/metadata.d.ts +109 -0
- package/dist/framework/utils/metadata.d.ts.map +1 -0
- package/dist/framework/utils/metadata.js +105 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/presets/index.d.ts +8 -0
- package/dist/presets/index.d.ts.map +1 -0
- package/dist/presets/index.js +7 -0
- package/dist/presets/marketing.d.ts +41 -0
- package/dist/presets/marketing.d.ts.map +1 -0
- package/dist/presets/marketing.js +81 -0
- package/dist/presets/product.d.ts +41 -0
- package/dist/presets/product.d.ts.map +1 -0
- package/dist/presets/product.js +74 -0
- package/package.json +91 -0
- package/src/framework/README.md +329 -0
- package/src/framework/__tests__/branding/css-vars.test.ts +147 -0
- package/src/framework/__tests__/components/ErrorBoundary.test.tsx +146 -0
- package/src/framework/__tests__/config/defineConfig.test.ts +138 -0
- package/src/framework/__tests__/hooks/useMotion.test.ts +105 -0
- package/src/framework/__tests__/seo/geo/generateGEOMetadata.test.ts +207 -0
- package/src/framework/__tests__/seo/geo/structuredData.test.ts +262 -0
- package/src/framework/a11y/components/LiveRegion.tsx +89 -0
- package/src/framework/a11y/components/SkipToContent.tsx +103 -0
- package/src/framework/a11y/hooks/useFocusManagement.ts +125 -0
- package/src/framework/a11y/hooks/useFocusTrap.ts +239 -0
- package/src/framework/a11y/hooks/useLiveRegion.ts +95 -0
- package/src/framework/a11y/index.ts +17 -0
- package/src/framework/branding/context.tsx +135 -0
- package/src/framework/branding/css-vars.ts +110 -0
- package/src/framework/branding/tailwind-config.ts +90 -0
- package/src/framework/components/BrandedButton.tsx +94 -0
- package/src/framework/components/BrandedCard.tsx +87 -0
- package/src/framework/components/ErrorBoundary.tsx +215 -0
- package/src/framework/components/HuaUxLayout.tsx +36 -0
- package/src/framework/components/HuaUxPage.tsx +138 -0
- package/src/framework/components/Providers.tsx +98 -0
- package/src/framework/components/WelcomePage.tsx +207 -0
- package/src/framework/config/index.ts +349 -0
- package/src/framework/config/merge.ts +190 -0
- package/src/framework/config/schema.ts +140 -0
- package/src/framework/hooks/useMotion.ts +57 -0
- package/src/framework/index.ts +122 -0
- package/src/framework/license/errors.ts +63 -0
- package/src/framework/license/index.ts +137 -0
- package/src/framework/license/loader.ts +158 -0
- package/src/framework/license/types.ts +95 -0
- package/src/framework/loading/components/SkeletonGroup.tsx +70 -0
- package/src/framework/loading/components/SuspenseWrapper.tsx +88 -0
- package/src/framework/loading/hoc/withSuspense.tsx +96 -0
- package/src/framework/loading/hooks/useDelayedLoading.ts +127 -0
- package/src/framework/loading/hooks/useLoadingState.ts +103 -0
- package/src/framework/loading/index.ts +19 -0
- package/src/framework/middleware/i18n.ts +161 -0
- package/src/framework/middleware/index.ts +7 -0
- package/src/framework/plugins/index.ts +13 -0
- package/src/framework/plugins/registry.ts +186 -0
- package/src/framework/plugins/types.ts +106 -0
- package/src/framework/seo/geo/examples.tsx +415 -0
- package/src/framework/seo/geo/generateGEOMetadata.ts +441 -0
- package/src/framework/seo/geo/index.ts +61 -0
- package/src/framework/seo/geo/presets.ts +58 -0
- package/src/framework/seo/geo/structuredData.ts +422 -0
- package/src/framework/seo/geo/test-utils.ts +179 -0
- package/src/framework/seo/geo/types.ts +315 -0
- package/src/framework/types/index.ts +623 -0
- package/src/framework/utils/data-fetching.ts +95 -0
- package/src/framework/utils/file-structure.ts +88 -0
- package/src/framework/utils/metadata.ts +152 -0
- package/src/index.ts +31 -0
- package/src/presets/index.ts +8 -0
- package/src/presets/marketing.ts +88 -0
- 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
|
+
}
|