1+ #! /bin/bash
2+
3+ # Script to generate HTML documentation from all API specs using Redocly
4+ # Preserves folder structure and generates a card-based index page
5+
6+ set -ex
7+ set -o pipefail
8+
9+ # Colors
10+ RED=' \033[0;31m'
11+ GREEN=' \033[0;32m'
12+ YELLOW=' \033[1;33m'
13+ BLUE=' \033[0;34m'
14+ NC=' \033[0m'
15+
16+ # Directories
17+ SPECS_DIR=" specs"
18+ OUTPUT_DIR=" docs/api-docs"
19+ INDEX_FILE=" $OUTPUT_DIR /index.html"
20+ ERROR_LOG=" $OUTPUT_DIR /errors.log"
21+
22+ echo -e " ${BLUE} 🚀 Starting API documentation generation...${NC} "
23+
24+ # Clean output folder
25+ rm -rf " $OUTPUT_DIR "
26+ mkdir -p " $OUTPUT_DIR "
27+
28+ # Check Redocly
29+ if ! command -v redocly & > /dev/null; then
30+ echo -e " ${RED} ❌ Redocly CLI not found. Install it:${NC} "
31+ echo " npm install -g @redocly/cli"
32+ exit 1
33+ fi
34+ echo -e " ${GREEN} ✅ Redocly found: $( redocly --version) ${NC} "
35+
36+ success_count=0
37+ error_count=0
38+ > " $ERROR_LOG "
39+
40+
41+ convert_spec_to_html () {
42+ local spec_file=" $1 "
43+ local relative_path=" ${spec_file# $SPECS_DIR / } "
44+ local output_file=" $OUTPUT_DIR /${relative_path% .* } .html"
45+ mkdir -p " $( dirname " $output_file " ) "
46+
47+ echo -e " ${BLUE} 📄 Converting: $spec_file ${NC} "
48+ echo -e " ${BLUE} → Output: ${relative_path% .* } .html${NC} "
49+
50+ if redocly build-docs " $spec_file " -o " $output_file " > /dev/null 2>&1 ; then
51+ echo -e " ${GREEN} ✅ Success: $output_file ${NC} "
52+ (( success_count++ ))
53+ else
54+ echo -e " ${RED} ❌ Failed: $spec_file ${NC} "
55+ echo " $spec_file " >> " $ERROR_LOG "
56+ (( error_count++ ))
57+ fi
58+ }
59+
60+ # Find spec files
61+ mapfile -t spec_files < <( find " $SPECS_DIR " -type f \( -name " *.yaml" -o -name " *.yml" \) | sort)
62+ echo -e " ${BLUE} 📊 Found ${# spec_files[@]} specs${NC} "
63+
64+ # Convert specs
65+ for spec_file in " ${spec_files[@]} " ; do
66+ convert_spec_to_html " $spec_file " || true
67+ done
68+
69+
70+
71+ # Generate index.html
72+ cat > " $INDEX_FILE " << 'EOF '
73+ <!DOCTYPE html>
74+ <html lang="en">
75+ <head>
76+ <meta charset="UTF-8">
77+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
78+ <title>Devtron API Documentation</title>
79+ <style>
80+ body { font-family: Arial, sans-serif; margin: 20px; background: #f8f9fa; color: #333; }
81+ h1 { text-align: center; color: #2c3e50; margin-bottom: 40px; }
82+ .container { max-width: 1200px; margin: auto; }
83+ .categories-grid {
84+ display: flex;
85+ flex-wrap: wrap;
86+ justify-content: center;
87+ gap: 30px;
88+ margin-top: 20px;
89+ align-items: stretch; /* Ensures all cards stretch to same height */
90+ }
91+
92+ /* Single card centering */
93+ .categories-grid.single-card {
94+ justify-content: center;
95+ max-width: 400px;
96+ margin: 20px auto;
97+ }
98+
99+ /* Category Cards */
100+ .category-card {
101+ background: #fff;
102+ border-radius: 12px;
103+ padding: 25px;
104+ width: calc(33.33% - 30px);
105+ min-width: 300px;
106+ max-width: 400px;
107+ height: auto;
108+ min-height: 300px; /* Minimum height for consistency */
109+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
110+ border: 1px solid #e1e5e9;
111+ transition: transform 0.2s ease, box-shadow 0.2s ease;
112+ display: flex;
113+ flex-direction: column;
114+ }
115+ .category-card:hover {
116+ transform: translateY(-2px);
117+ box-shadow: 0 6px 20px rgba(0,0,0,0.15);
118+ }
119+
120+ /* Single card styling */
121+ .category-card.single {
122+ width: 100%;
123+ max-width: 400px;
124+ }
125+
126+ /* Category Headers */
127+ .category-header {
128+ color: #2c3e50;
129+ font-size: 1.4em;
130+ font-weight: bold;
131+ margin-bottom: 15px;
132+ padding-bottom: 10px;
133+ border-bottom: 2px solid #3498db;
134+ text-align: center;
135+ }
136+
137+ /* API Links within Categories */
138+ .api-links {
139+ display: flex;
140+ flex-direction: column;
141+ gap: 8px;
142+ flex-grow: 1; /* Takes up remaining space in the card */
143+ overflow-y: auto; /* Allows scrolling if too many links */
144+ max-height: 400px; /* Maximum height before scrolling */
145+ }
146+ .api-link {
147+ display: block;
148+ padding: 8px 12px;
149+ background: #f8f9fa;
150+ border-radius: 6px;
151+ text-decoration: none;
152+ color: #1a73e8;
153+ font-weight: 500;
154+ transition: all 0.2s ease;
155+ border-left: 3px solid transparent;
156+ }
157+ .api-link:hover {
158+ background: #e3f2fd;
159+ border-left-color: #1a73e8;
160+ text-decoration: none;
161+ transform: translateX(5px);
162+ }
163+
164+ /* Footer */
165+ .footer {
166+ margin-top: 50px;
167+ font-size: 0.9rem;
168+ color: #666;
169+ text-align: center;
170+ padding-top: 20px;
171+ border-top: 1px solid #e1e5e9;
172+ }
173+ .footer a { color: #1a73e8; text-decoration: none; }
174+ .footer a:hover { text-decoration: underline; }
175+ .timestamp { font-style: italic; }
176+
177+ /* Responsive Design */
178+ @media(max-width: 1024px){
179+ .category-card { width: calc(50% - 30px); }
180+ .categories-grid.single-card { max-width: 500px; }
181+ }
182+ @media(max-width: 768px){
183+ .category-card { width: 100%; min-width: unset; max-width: none; }
184+ .categories-grid.single-card { max-width: 100%; margin: 20px 0; }
185+ }
186+ @media(max-width: 480px){
187+ .category-card { margin: 0 10px; }
188+ .categories-grid { gap: 20px; }
189+ }
190+ </style>
191+ </head>
192+ <body>
193+ <div class="container">
194+ <h1> Devtron API Documentation</h1>
195+ <div id="categories" class="categories-grid"></div>
196+ <div class="footer">
197+ <p><a href="https://devtron.ai/" target="_blank">Devtron</a></p>
198+ <p class="timestamp">Last updated: <span id="timestamp"></span></p>
199+ </div>
200+ </div>
201+ <script>
202+ const apiData = {
203+ EOF
204+
205+
206+
207+ # Populate apiData preserving folder structure
208+ for spec_file in " ${spec_files[@]} " ; do
209+ relative_path=" ${spec_file# $SPECS_DIR / } "
210+ html_file=" ${relative_path% .* } .html"
211+ category=$( dirname " $relative_path " )
212+ [[ " $category " == " ." ]] && category=" Root"
213+
214+ display_category=$( echo " $category " | sed ' s/[-_]/ /g' | sed ' s/\([a-z]\)\([A-Z]\)/\1 \2/g' | sed ' s/\b\w/\U&/g' )
215+ title=$( grep -m 1 ' ^[[:space:]]*title:' " $spec_file " | sed ' s/^[[:space:]]*title:[[:space:]]*//' | tr -d ' "' || echo " ${relative_path% .* } " )
216+
217+ # Only include if HTML file was successfully generated
218+ if [[ -f " $OUTPUT_DIR /$html_file " ]]; then
219+ # Ensure proper relative path from index.html to the generated HTML file
220+ # Since index.html is in docs/api-docs/ and HTML files maintain folder structure
221+ echo " \" ${category} _$( basename " ${relative_path% .* } " ) \" : {\" category\" : \" ${display_category} \" , \" title\" : \" ${title} \" , \" filename\" : \" ${html_file} \" }," >> " $INDEX_FILE "
222+ fi
223+ done
224+
225+ sed -i ' $ s/,$//' " $INDEX_FILE "
226+
227+
228+
229+ cat >> " $INDEX_FILE " << 'EOF '
230+ };
231+
232+ function populatePage() {
233+ const container = document.getElementById('categories');
234+ const categories = {};
235+
236+ // Group APIs by category
237+ Object.values(apiData).forEach(api => {
238+ if (!categories[api.category]) categories[api.category] = [];
239+ categories[api.category].push(api);
240+ });
241+
242+ const categoryNames = Object.keys(categories).sort();
243+
244+ // Add class for single card centering
245+ if (categoryNames.length === 1) {
246+ container.classList.add('single-card');
247+ }
248+
249+ // Create category cards
250+ categoryNames.forEach(categoryName => {
251+ // Create category card
252+ const categoryCard = document.createElement('div');
253+ categoryCard.className = 'category-card';
254+
255+ // Add single class if only one card
256+ if (categoryNames.length === 1) {
257+ categoryCard.classList.add('single');
258+ }
259+
260+ // Create category header
261+ const categoryHeader = document.createElement('div');
262+ categoryHeader.className = 'category-header';
263+ categoryHeader.textContent = categoryName;
264+ categoryCard.appendChild(categoryHeader);
265+
266+ // Create links container
267+ const linksContainer = document.createElement('div');
268+ linksContainer.className = 'api-links';
269+
270+ // Add API links to this category
271+ categories[categoryName]
272+ .sort((a, b) => a.title.localeCompare(b.title))
273+ .forEach(api => {
274+ const apiLink = document.createElement('a');
275+ // Ensure proper relative path
276+ apiLink.href = api.filename;
277+ apiLink.textContent = api.title;
278+ apiLink.className = 'api-link';
279+ apiLink.title = `View ${api.title} API documentation`;
280+
281+ // Add click handler to check if file exists
282+ apiLink.addEventListener('click', function(e) {
283+ // Let the browser handle the navigation normally
284+ // This is just for debugging - remove in production if needed
285+ console.log(`Navigating to: ${api.filename}`);
286+ });
287+
288+ linksContainer.appendChild(apiLink);
289+ });
290+
291+ categoryCard.appendChild(linksContainer);
292+ container.appendChild(categoryCard);
293+ });
294+
295+ document.getElementById('timestamp').textContent = new Date().toLocaleString();
296+ }
297+
298+ document.addEventListener('DOMContentLoaded', populatePage);
299+ </script>
300+ </body>
301+ </html>
302+ EOF
303+
304+
305+
306+ echo -e " ${GREEN} ✅ Card-based index page generated: $INDEX_FILE ${NC} "
307+
308+ # === SUMMARY ===
309+ echo -e " ${BLUE} 📊 Final Summary:${NC} "
310+ echo -e " ${GREEN} ✅ Successfully converted: $success_count specs${NC} "
311+ if (( error_count > 0 )) ; then
312+ echo -e " ${RED} ❌ Failed: $error_count (see $ERROR_LOG )${NC} "
313+ fi
314+ echo -e " ${BLUE} 📁 Output directory: $OUTPUT_DIR ${NC} "
315+ echo -e " ${BLUE} 🌐 Main index: $INDEX_FILE ${NC} "
316+
317+ # === CREATE README ===
318+ cat > " $OUTPUT_DIR /README.md" << 'EOF '
319+ # Devtron API Documentation
320+
321+ This folder contains the HTML documentation generated from the OpenAPI specs in the `specs` directory.
322+ EOF
323+
324+ echo -e " ${GREEN} ✅ README created: $OUTPUT_DIR /README.md${NC} "
325+ echo -e " ${GREEN} 🎉 API documentation generation complete!${NC} "
0 commit comments