Back to library

API & Routes - Database

Hướng dẫn chung
Usage: 5
Likes: 5
Updated: 27 April, 2026

Hướng dẫn cho AI biết cách sử dụng database

Prompt Structure (Content)

# APPIFIO App Storage API - Complete AI Developer Guide ## 🚫 CRITICAL: THIS IS USAGE DOCUMENTATION **AppStorageClient is PRE-BUILT and AUTO-INJECTED into your application.** ### Your Role: - ✅ **USE** existing methods - ✅ **CALL** `appifio_client.appifio_*()` functions - ❌ **NEVER** define `class AppStorageClient` - ❌ **NEVER** rewrite `appifio_*` methods --- ## 🌐 GLOBAL VARIABLES & FUNCTIONS (Auto-Injected by System) ### 1. Global Configuration Object The system automatically injects this into every page: ```javascript window.appifio = { global: { apps: { url_name: "test-blog", // Current app URL name api_url: "https://appifio.com/l/link/app_storage", // API endpoint site_url: "https://appifio.com/", // Base site URL api_key: "encoded_api_key_here" // Encoded API key } } } ``` **Usage:** ```javascript // Get URL name const urlName = window.appifio.global.apps.url_name; // Get site URL const siteUrl = window.appifio.global.apps.site_url; // Get API URL const apiUrl = window.appifio.global.apps.api_url; ``` ### 2. AppStorage Client Instance The system creates `appifio_client` as a getter property: ```javascript // ✅ CORRECT - appifio_client is always available (never undefined) if (appifio_client && appifio_client.appifio_readFile) { const result = await appifio_client.appifio_readFile('blog.json'); } // ❌ WRONG - Don't check typeof appifio_client === 'undefined' // It's a getter property, always exists (returns proxyClient if not ready) ``` **Important:** `appifio_client` may return a proxy client that queues calls until the real client is ready. Always check for method existence, not the variable itself. ### 3. Global Helper Functions Available from `biolink_handler.php`: ```javascript // Wait for AppStorage client to be ready await window.waitForAppStorage(); // Callback when AppStorage is ready window.onAppStorageReady((client) => { console.log('Client ready:', client); }); // Get client instance (may return proxy) const client = window.getAppStorageClient(); // Get client with callback await window.withClient(async (client) => { await client.appifio_readFile('blog.json'); }); ``` **Recommended Pattern:** ```javascript // Wait for client to be ready try { if (typeof window.waitForAppStorage === 'function') { await window.waitForAppStorage(); } else if (typeof window.onAppStorageReady === 'function') { await new Promise((resolve) => { window.onAppStorageReady(() => resolve()); }); } else { // Fallback: wait for appifio_client methods let retries = 0; while ((!appifio_client || !appifio_client.appifio_readFile) && retries < 50) { await new Promise(resolve => setTimeout(resolve, 100)); retries++; } } } catch (error) { console.warn('Error waiting for AppStorage:', error); } ``` --- ## 🔗 ABSOLUTE URL HANDLING ### ⚠️ CRITICAL: Base Tag Issue The system uses a `<base>` tag that affects relative URLs. **Always use absolute URLs** for navigation links. ### Helper Function: Get Absolute URL ```javascript function getAbsoluteUrl(path) { // Get site URL from global config let siteUrl = window.location.origin; if (window.appifio && window.appifio.global && window.appifio.global.apps && window.appifio.global.apps.site_url) { siteUrl = window.appifio.global.apps.site_url.replace(/\/$/, ''); // Remove trailing slash } // Get URL name let urlName = ''; if (window.appifio && window.appifio.global && window.appifio.global.apps && window.appifio.global.apps.url_name) { urlName = window.appifio.global.apps.url_name; } else { // Fallback: extract from current URL const pathParts = window.location.pathname.split('/').filter(p => p); urlName = pathParts[0] || ''; } // Build absolute URL return siteUrl + '/' + urlName + '/' + path.replace(/^\//, ''); } // Usage examples: const homeUrl = getAbsoluteUrl(''); // https://appifio.com/test-blog/ const blogUrl = getAbsoluteUrl('my-blog'); // https://appifio.com/test-blog/my-blog const tagUrl = getAbsoluteUrl('tag/javascript'); // https://appifio.com/test-blog/tag/javascript ``` ### Helper Function: Get URL Name ```javascript function getUrlName() { if (window.appifio && window.appifio.global && window.appifio.global.apps && window.appifio.global.apps.url_name) { return window.appifio.global.apps.url_name; } // Fallback: extract from URL const pathParts = window.location.pathname.split('/').filter(p => p); return pathParts[0] || ''; } ``` ### Example: Back Link with Absolute URL ```javascript function updateBackLink() { const backLink = document.getElementById('back-link'); if (backLink) { const urlName = getUrlName(); let siteUrl = window.location.origin; if (window.appifio && window.appifio.global && window.appifio.global.apps && window.appifio.global.apps.site_url) { siteUrl = window.appifio.global.apps.site_url.replace(/\/$/, ''); } // Use absolute path to avoid base tag issues backLink.href = siteUrl + '/' + urlName + '/'; } } ``` --- ## ⚠️ CRITICAL CONCEPT 1: File Not Found is NORMAL ### Understanding First-Time Usage **When reading a file that doesn't exist yet, the API returns `success: false`.** This is **NORMAL** for first-time usage. You must handle this gracefully. ### ✅ CORRECT PATTERN: Always Initialize with Default Structure ```javascript // ✅ CORRECT - Always start with default structure async function loadBlogs() { // 1. Initialize with default structure FIRST let blogIndex = { blogs: {}, metadata: { total: 0, last_updated: '' } } // 2. Try to read existing file try { if (appifio_client && appifio_client.appifio_readFile) { const result = await appifio_client.appifio_readFile('blog.json') // 3. Only use data if read was successful if (result && result.success) { blogIndex = JSON.parse(result.data.content) } else { // File doesn't exist yet - this is OK! console.log('blog.json not found, using empty structure') } } } catch (error) { // Also OK - file doesn't exist console.log('blog.json not found:', error.message) } // 4. Continue with blogIndex (empty or loaded) return blogIndex } ``` ### ❌ WRONG PATTERN: Don't Assume File Exists ```javascript // ❌ WRONG - Will crash if file doesn't exist const result = await appifio_client.appifio_readFile('blog.json') const blogIndex = JSON.parse(result.data.content) // CRASH if result.success === false! ``` --- ## ⚠️ CRITICAL CONCEPT 2: Routes are MANDATORY ### Understanding the Route System **IMPORTANT: The system ONLY reads `index.html` by default!** - ✅ URL: `/urlname/` → Reads: `index.html` (automatic) - ❌ URL: `/urlname/blog-detail` → **NOTHING** (no file served) - ✅ URL: `/urlname/blog-detail` → Reads: `blog/detail.html` (IF route exists) ### Why Routes are Required **Without routes, only `index.html` works. ALL other URLs return 404!** To make ANY other page work, you MUST: 1. Create the HTML file in filesystem 2. Add route mapping to `routes.json` ### Route System Architecture ``` User visits: /urlname/my-blog-post ↓ System checks: routes.json ↓ Finds: "my-blog-post" → { "file": "blog/detail.html" } ↓ Serves: blog/detail.html ↓ blog/detail.html reads data from blog.json ↓ Displays blog content ``` ### ⚠️ CRITICAL: Nested Route Paths **Route paths can contain slashes for nested routes:** ```javascript // ✅ CORRECT - Nested routes work await appifio_client.appifio_addRoute('tag/javascript', 'blog/tag.html', 'tag'); await appifio_client.appifio_addRoute('category/tech', 'blog/category.html', 'category'); // URL: /urlname/tag/javascript → Loads blog/tag.html // URL: /urlname/category/tech → Loads blog/category.html ``` **The routing system extracts the full path after `urlname`:** - URL: `/test-blog/tag/3123` - Route path extracted: `tag/3123` - Matches route in `routes.json`: `"tag/3123"` ### routes.json Structure ```json { "routes": { "my-blog-post": { "file": "blog/detail.html", "type": "blog", "enabled": true }, "tag/javascript": { "file": "blog/tag.html", "type": "tag", "enabled": true }, "category/tech": { "file": "blog/category.html", "type": "category", "enabled": true } }, "fallback": "index.html" } ``` ### ✅ MANDATORY: Always Create Routes **Every time you create content, you MUST add a route!** ```javascript // ❌ WRONG - No route created, URL won't work await appifio_client.appifio_writeFile('blog.json', blogData) // User visits /urlname/my-blog → 404 ERROR! // ✅ CORRECT - Route created, URL works await appifio_client.appifio_writeFile('blog.json', blogData) await appifio_client.appifio_addRoute('my-blog', 'blog/detail.html', 'blog') // User visits /urlname/my-blog → ✅ Works! ``` --- ## 📚 AVAILABLE API METHODS ### A. TempDB Storage Operations (Data Files) Store JSON data files like `blog.json`, `settings.json`: ```javascript // Read file const result = await appifio_client.appifio_readFile('blog.json') if (result && result.success) { const data = JSON.parse(result.data.content) } // Write file (creates if doesn't exist, overwrites if exists) const saveResult = await appifio_client.appifio_writeFile('blog.json', JSON.stringify(data, null, 2)) if (!saveResult.success) { console.error('Save failed:', saveResult.message) } // Delete file await appifio_client.appifio_deleteFile('blog.json') // List files const files = await appifio_client.appifio_listFiles() // File exists check const exists = await appifio_client.appifio_fileExists('blog.json') // Get file info const info = await appifio_client.appifio_getFileInfo('blog.json') ``` ### B. Filesystem Operations (HTML, Routes) Store HTML files, routes.json in filesystem: ```javascript // Read from filesystem const result = await appifio_client.appifio_readFsFile('routes.json') if (result && result.success) { const routes = JSON.parse(result.data.content) } // Write to filesystem await appifio_client.appifio_writeFsFile('routes.json', JSON.stringify(routes, null, 2)) // Delete from filesystem await appifio_client.appifio_deleteFsFile('file.html') // List filesystem files const fsFiles = await appifio_client.appifio_listFsFiles() // Get filesystem file info const fsInfo = await appifio_client.appifio_getFsFileInfo('routes.json') ``` ### C. Route Management (MANDATORY) ```javascript // Add route (REQUIRED for every content) const routeResult = await appifio_client.appifio_addRoute( 'my-blog-post', // Route path (can be nested: 'tag/javascript') 'blog/detail.html', // Target HTML file 'blog' // Route type (optional, for categorization) ) if (!routeResult.success) { console.warn('Failed to add route:', routeResult.message) } // Remove route (MANDATORY when deleting content) const removeResult = await appifio_client.appifio_removeRoute('my-blog-post') // Get all routes const routesResult = await appifio_client.appifio_getRoutes() if (routesResult.success && routesResult.data) { const routes = routesResult.data.routes || {} console.log('All routes:', routes) } ``` ### D. Helper Functions ```javascript // Generate slug (supports Vietnamese) const slug = appifio_client.appifio_generateSlug("Bài viết về AI") // Returns: "bai-viet-ve-ai" // Format datetime const formatted = appifio_client.appifio_formatDateTime('2025-12-27 15:59:10', 'date') // Returns: "27 Dec 2025" // Format file size const size = appifio_client.appifio_formatFileSize(1024) // Returns: "1 KB" // Get route slug from current URL const currentSlug = appifio_client.appifio_getRouteSlug() // Returns: "my-blog-post" for URL /urlname/my-blog-post // Parse query parameters const params = appifio_client.appifio_getQueryParams() // Returns: { id: "123", page: "2" } for URL ?id=123&page=2 ``` ### E. Upload Operations ```javascript // Upload image const fileInput = document.getElementById('imageInput') const uploadResult = await appifio_client.appifio_uploadImage(fileInput.files[0]) if (uploadResult.success) { console.log('Image URL:', uploadResult.data.url) } // Upload file const fileResult = await appifio_client.appifio_uploadFile(fileInput.files[0]) ``` ### F. Auxiliary Operations (Advanced) These methods are available for more specific file manipulations. ```javascript // 1. Copy File // Copies 'source.json' to 'backup.json' const copyResult = await appifio_client.appifio_copyFile('source.json', 'backup.json') // 2. Rename File // Renames 'old_name.json' to 'new_name.json' const renameResult = await appifio_client.appifio_renameFile('old_name.json', 'new_name.json') // 3. Search Files // Search for files containing "keyword" in name or content const searchResult = await appifio_client.appifio_searchFiles('keyword') // Returns array of matching files // 4. Read Specific Field (JSON Optimization) // Only reads the "metadata.total" field from "blog.json" instead of downloading the whole file // Useful for large JSON files const fieldResult = await appifio_client.appifio_readField('blog.json', 'metadata.total') // 5. Write Specific Field (JSON Optimization) // Updates only "metadata.last_updated" in "blog.json" const updateFieldResult = await appifio_client.appifio_writeField('blog.json', 'metadata.last_updated', '2025-12-31') ``` --- ## 🎯 DATA STRUCTURE PATTERN: Single Index File ### ⚠️ CRITICAL CONCEPT **Use ONE file to store ALL entries of the same type.** ### Example: Blog System with Tags and Categories **Structure:** ``` TempDB Storage: ├── blog.json ← ALL blogs stored here Filesystem: ├── routes.json ← Route mappings (MANDATORY) ├── index.html ← Main page └── blog/ ├── detail.html ← Blog detail page handler ├── tag.html ← Tag listing page handler └── category.html ← Category listing page handler ``` **blog.json content:** ```json { "blogs": { "1735302550000": { "id": "1735302550000", "title": "Bài viết về AI", "slug": "bai-viet-ve-ai", "author": "Admin", "content": "<p>Nội dung đầy đủ của bài viết...</p>", "excerpt": "Mô tả ngắn gọn...", "tags": ["AI", "Technology", "JavaScript"], "category": "Công nghệ", "created_at": "2025-12-27 15:59:10", "updated_at": "2025-12-27 15:59:10" } }, "metadata": { "total": 1, "last_updated": "2025-12-27 15:59:10" } } ``` **routes.json content (MANDATORY):** ```json { "routes": { "bai-viet-ve-ai": { "file": "blog/detail.html", "type": "blog", "enabled": true }, "tag/javascript": { "file": "blog/tag.html", "type": "tag", "enabled": true }, "category/cong-nghe": { "file": "blog/category.html", "type": "category", "enabled": true } }, "fallback": "index.html" } ``` --- ## 💻 COMPLETE IMPLEMENTATION EXAMPLES ### Example 1: Create Blog with Tags and Categories ```javascript async function createBlog(title, author, content, tags = [], category = 'Uncategorized') { try { // STEP 1: Initialize with empty structure let blogIndex = { blogs: {}, metadata: { total: 0, last_updated: '' } } // STEP 2: Try to read existing file try { if (appifio_client && appifio_client.appifio_readFile) { const result = await appifio_client.appifio_readFile('blog.json') if (result && result.success) { blogIndex = JSON.parse(result.data.content) } } } catch (error) { console.log('Creating new blog.json') } // STEP 3: Generate ID and slug const blogId = Date.now().toString() const slug = appifio_client.appifio_generateSlug(title) // STEP 4: Create excerpt const tempDiv = document.createElement('div') tempDiv.innerHTML = content const textContent = tempDiv.textContent || tempDiv.innerText || '' const excerpt = textContent.substring(0, 150) + (textContent.length > 150 ? '...' : '') // STEP 5: Create blog entry const now = new Date().toISOString().slice(0, 19).replace('T', ' ') blogIndex.blogs[blogId] = { id: blogId, title: title, slug: slug, author: author, content: content, excerpt: excerpt, tags: tags, category: category, created_at: now, updated_at: now } // STEP 6: Update metadata blogIndex.metadata.total = Object.keys(blogIndex.blogs).length blogIndex.metadata.last_updated = now // STEP 7: Save to TempDB const saveResult = await appifio_client.appifio_writeFile( 'blog.json', JSON.stringify(blogIndex, null, 2) ) if (!saveResult.success) { throw new Error('Failed to save: ' + saveResult.message) } // STEP 8: MANDATORY - Add route for blog post const routeResult = await appifio_client.appifio_addRoute( slug, 'blog/detail.html', 'blog' ) if (!routeResult.success) { console.warn('Failed to add blog route:', routeResult.message) } // STEP 9: MANDATORY - Ensure routes for tags await ensureTagRoutes(tags) // STEP 10: MANDATORY - Ensure route for category await ensureCategoryRoute(category) console.log('Blog created successfully!') return { success: true, blog: blogIndex.blogs[blogId] } } catch (error) { console.error('Error creating blog:', error) return { success: false, error: error.message } } } // Helper: Ensure tag routes exist async function ensureTagRoutes(tags) { if (!tags || tags.length === 0) return if (!appifio_client || !appifio_client.appifio_addRoute) { console.warn('AppStorage client not available') return } for (const tag of tags) { if (!tag || tag.trim() === '') continue const tagSlug = appifio_client.appifio_generateSlug(tag) if (!tagSlug) continue const tagRoute = 'tag/' + tagSlug // Check if route already exists try { const routesResult = await appifio_client.appifio_getRoutes() if (routesResult.success && routesResult.data) { const routes = routesResult.data.routes || {} if (routes[tagRoute]) { console.log('Tag route already exists:', tagRoute) continue } } // Create route for tag const routeResult = await appifio_client.appifio_addRoute( tagRoute, 'blog/tag.html', 'tag' ) if (routeResult.success) { console.log('Tag route created:', tagRoute) } else { console.warn('Failed to create tag route:', tagRoute, routeResult.message) } } catch (error) { console.error('Error creating tag route:', tag, error) } } } // Helper: Ensure category route exists async function ensureCategoryRoute(category) { if (!category || category.trim() === '') return if (!appifio_client || !appifio_client.appifio_addRoute) { console.warn('AppStorage client not available') return } const categorySlug = appifio_client.appifio_generateSlug(category) if (!categorySlug) return const categoryRoute = 'category/' + categorySlug // Check if route already exists try { const routesResult = await appifio_client.appifio_getRoutes() if (routesResult.success && routesResult.data) { const routes = routesResult.data.routes || {} if (routes[categoryRoute]) { console.log('Category route already exists:', categoryRoute) return } } // Create route for category const routeResult = await appifio_client.appifio_addRoute( categoryRoute, 'blog/category.html', 'category' ) if (routeResult.success) { console.log('Category route created:', categoryRoute) } else { console.warn('Failed to create category route:', categoryRoute, routeResult.message) } } catch (error) { console.error('Error creating category route:', category, error) } } ``` ### Example 2: Delete Blog (with Route Removal) ```javascript async function deleteBlog(blogId) { if (!confirm('Bạn có chắc muốn xóa bài viết này?')) { return } try { // STEP 1: Read current data if (!appifio_client || !appifio_client.appifio_readFile) { throw new Error('AppStorage client not available') } const result = await appifio_client.appifio_readFile('blog.json') if (!result || !result.success) { throw new Error('Blog index not found') } const blogIndex = JSON.parse(result.data.content) // STEP 2: Check if blog exists if (!blogIndex.blogs[blogId]) { throw new Error('Blog not found') } // STEP 3: Get slug (for route removal) const slug = blogIndex.blogs[blogId].slug // STEP 4: Remove from index delete blogIndex.blogs[blogId] // STEP 5: Update metadata const now = new Date().toISOString().slice(0, 19).replace('T', ' ') blogIndex.metadata.total = Object.keys(blogIndex.blogs).length blogIndex.metadata.last_updated = now // STEP 6: Save to TempDB const saveResult = await appifio_client.appifio_writeFile( 'blog.json', JSON.stringify(blogIndex, null, 2) ) if (!saveResult.success) { throw new Error('Failed to save: ' + saveResult.message) } // STEP 7: MANDATORY - Remove route if (appifio_client.appifio_removeRoute) { const routeResult = await appifio_client.appifio_removeRoute(slug) if (!routeResult.success) { console.warn('Route removal failed:', routeResult.message) } else { console.log('Route removed:', slug) } } console.log('Blog deleted successfully') alert('Đã xóa bài viết thành công') return { success: true } } catch (error) { console.error('Error deleting blog:', error) alert('Lỗi: ' + error.message) return { success: false, error: error.message } } } ``` ### Example 3: Blog Detail Page (blog/detail.html) ```html <!DOCTYPE html> <html lang="vi"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Blog Detail</title> <style> body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } .blog-header { border-bottom: 2px solid #333; padding-bottom: 20px; margin-bottom: 20px; } .blog-meta { color: #666; font-size: 0.9em; margin-top: 10px; } .blog-content { margin-top: 30px; } .blog-tags { margin-top: 30px; } .tag { display: inline-block; background: #e0e0e0; padding: 5px 10px; margin-right: 5px; border-radius: 3px; } .loading { text-align: center; padding: 50px; } .error { color: red; text-align: center; padding: 50px; } </style> </head> <body> <div id="loading" class="loading">Đang tải...</div> <div id="error" class="error" style="display: none;">Không tìm thấy bài viết</div> <div id="blog-container" style="display: none;"> <div class="blog-header"> <h1 id="blog-title"></h1> <div class="blog-meta"> <span id="blog-author"></span> • <span id="blog-date"></span> </div> </div> <div class="blog-content" id="blog-content"></div> <div class="blog-tags" id="blog-tags"></div> <div style="margin-top: 50px;"> <a id="back-link" href="#">← Quay lại trang chủ</a> </div> </div> <script> // Helper: Get URL name function getUrlName() { if (window.appifio && window.appifio.global && window.appifio.global.apps && window.appifio.global.apps.url_name) { return window.appifio.global.apps.url_name; } const pathParts = window.location.pathname.split('/').filter(p => p); return pathParts[0] || ''; } // Helper: Update back link with absolute URL function updateBackLink() { const backLink = document.getElementById('back-link'); if (backLink) { const urlName = getUrlName(); let siteUrl = window.location.origin; if (window.appifio && window.appifio.global && window.appifio.global.apps && window.appifio.global.apps.site_url) { siteUrl = window.appifio.global.apps.site_url.replace(/\/$/, ''); } // Use absolute path to avoid base tag issues backLink.href = siteUrl + '/' + urlName + '/'; } } // Wait for AppStorage to be ready document.addEventListener('DOMContentLoaded', async function() { try { updateBackLink(); // Wait for AppStorage if (typeof window.waitForAppStorage === 'function') { await window.waitForAppStorage(); } else if (typeof window.onAppStorageReady === 'function') { await new Promise((resolve) => { window.onAppStorageReady(() => resolve()); }); } else { let retries = 0; while ((!appifio_client || !appifio_client.appifio_readFile) && retries < 50) { await new Promise(resolve => setTimeout(resolve, 100)); retries++; } } // Get slug from URL // URL format: /urlname/slug const pathParts = window.location.pathname.split('/').filter(p => p); let slug = ''; // Skip urlname, get the next part if (pathParts.length >= 2) { const secondPart = pathParts[1]; // If not 'tag' or 'category', it's a blog slug if (secondPart !== 'tag' && secondPart !== 'category') { slug = secondPart; } } console.log('Loading blog with slug:', slug); if (!slug) { showError('Không tìm thấy bài viết'); return; } // Load blog.json if (!appifio_client || !appifio_client.appifio_readFile) { showError('AppStorage không khả dụng'); return; } const result = await appifio_client.appifio_readFile('blog.json'); if (!result || !result.success) { showError('Không tìm thấy dữ liệu blog'); return; } const blogIndex = JSON.parse(result.data.content); // Find blog by slug const blog = Object.values(blogIndex.blogs).find(b => b.slug === slug); if (!blog) { showError('Không tìm thấy bài viết'); return; } // Display blog displayBlog(blog); } catch (error) { console.error('Error loading blog:', error); showError('Lỗi: ' + error.message); } }); function displayBlog(blog) { document.getElementById('loading').style.display = 'none'; document.getElementById('blog-container').style.display = 'block'; document.getElementById('blog-title').textContent = blog.title; document.getElementById('blog-author').textContent = 'Tác giả: ' + blog.author; // Format date let dateStr = blog.created_at; if (appifio_client && appifio_client.appifio_formatDateTime) { dateStr = appifio_client.appifio_formatDateTime(blog.created_at, 'date'); } document.getElementById('blog-date').textContent = dateStr; // Content document.getElementById('blog-content').innerHTML = blog.content; // Tags with links if (blog.tags && blog.tags.length > 0) { const urlName = getUrlName(); let siteUrl = window.location.origin; if (window.appifio && window.appifio.global && window.appifio.global.apps && window.appifio.global.apps.site_url) { siteUrl = window.appifio.global.apps.site_url.replace(/\/$/, ''); } const tagsHtml = blog.tags.map(tag => { const tagSlug = appifio_client.appifio_generateSlug(tag); const tagUrl = siteUrl + '/' + urlName + '/tag/' + tagSlug; return `<a href="${tagUrl}" class="tag">${escapeHtml(tag)}</a>`; }).join(''); document.getElementById('blog-tags').innerHTML = 'Tags: ' + tagsHtml; } } function showError(message) { document.getElementById('loading').style.display = 'none'; const errorDiv = document.getElementById('error'); errorDiv.textContent = message; errorDiv.style.display = 'block'; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } </script> </body> </html> ``` ### Example 4: Tag Listing Page (blog/tag.html) ```html <!DOCTYPE html> <html lang="vi"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Tag: JavaScript</title> <style> body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } .blog-item { border-bottom: 1px solid #eee; padding: 20px 0; } .blog-item h3 { margin: 0 0 10px 0; } .blog-item a { text-decoration: none; color: #333; } .blog-meta { color: #666; font-size: 0.9em; } .loading { text-align: center; padding: 50px; } .error { color: red; text-align: center; padding: 50px; } </style> </head> <body> <div id="loading" class="loading">Đang tải...</div> <div id="error" class="error" style="display: none;"></div> <div id="content" style="display: none;"> <h1 id="tag-title">Bài viết với thẻ</h1> <div id="blog-list"></div> <div style="margin-top: 30px;"> <a id="back-link" href="#">← Quay lại trang chủ</a> </div> </div> <script> // Helper functions function getUrlName() { if (window.appifio && window.appifio.global && window.appifio.global.apps && window.appifio.global.apps.url_name) { return window.appifio.global.apps.url_name; } const pathParts = window.location.pathname.split('/').filter(p => p); return pathParts[0] || ''; } function updateBackLink() { const backLink = document.getElementById('back-link'); if (backLink) { const urlName = getUrlName(); let siteUrl = window.location.origin; if (window.appifio && window.appifio.global && window.appifio.global.apps && window.appifio.global.apps.site_url) { siteUrl = window.appifio.global.apps.site_url.replace(/\/$/, ''); } backLink.href = siteUrl + '/' + urlName + '/'; } } function generateSlug(text) { if (!text) return ''; if (appifio_client && appifio_client.appifio_generateSlug) { return appifio_client.appifio_generateSlug(text); } return text.toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/[^\w\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-') .replace(/^-+|-+$/g, ''); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Main script document.addEventListener('DOMContentLoaded', async function() { try { updateBackLink(); // Get tag slug from URL // URL format: /urlname/tag/tag-slug const pathParts = window.location.pathname.split('/').filter(p => p); let tagSlug = ''; const tagIndex = pathParts.indexOf('tag'); if (tagIndex !== -1 && pathParts.length > tagIndex + 1) { tagSlug = pathParts[tagIndex + 1]; } console.log('Loading blogs with tag slug:', tagSlug); if (!tagSlug) { showError('Không tìm thấy thẻ'); return; } // Wait for AppStorage if (typeof window.waitForAppStorage === 'function') { await window.waitForAppStorage(); } else if (typeof window.onAppStorageReady === 'function') { await new Promise((resolve) => { window.onAppStorageReady(() => resolve()); }); } else { let retries = 0; while ((!appifio_client || !appifio_client.appifio_readFile) && retries < 50) { await new Promise(resolve => setTimeout(resolve, 100)); retries++; } } await loadTagBlogs(tagSlug); } catch (error) { console.error('Error:', error); showError('Lỗi khi tải bài viết'); } }); async function loadTagBlogs(tagSlug) { try { let blogIndex = { blogs: {}, metadata: { total: 0 } }; if (appifio_client && appifio_client.appifio_readFile) { const result = await appifio_client.appifio_readFile('blog.json'); if (result && result.success) { blogIndex = JSON.parse(result.data.content); } else { showError('Không tìm thấy dữ liệu blog'); return; } } else { showError('AppStorage không khả dụng'); return; } // Filter blogs by tag const tagBlogs = Object.values(blogIndex.blogs).filter(blog => { if (!blog.tags || !Array.isArray(blog.tags)) return false; return blog.tags.some(tag => { const tagSlugNormalized = generateSlug(tag); return tagSlugNormalized === tagSlug; }); }); if (tagBlogs.length === 0) { showError('Không tìm thấy bài viết nào với thẻ này'); return; } // Get tag name from first blog const firstBlog = tagBlogs[0]; const tagName = firstBlog.tags.find(tag => { const tagSlugNormalized = generateSlug(tag); return tagSlugNormalized === tagSlug; }); // Display blogs displayTagBlogs(tagName, tagBlogs); } catch (error) { console.error('Error loading tag blogs:', error); showError('Lỗi: ' + error.message); } } function displayTagBlogs(tagName, blogs) { document.getElementById('loading').style.display = 'none'; document.getElementById('content').style.display = 'block'; document.getElementById('tag-title').textContent = 'Bài viết với thẻ: ' + tagName; document.title = 'Thẻ: ' + tagName + ' - Blog'; const blogList = document.getElementById('blog-list'); const urlName = getUrlName(); let siteUrl = window.location.origin; if (window.appifio && window.appifio.global && window.appifio.global.apps && window.appifio.global.apps.site_url) { siteUrl = window.appifio.global.apps.site_url.replace(/\/$/, ''); } blogs.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); let html = ''; blogs.forEach(blog => { html += ` <div class="blog-item"> <h3><a href="${siteUrl}/${urlName}/${blog.slug}">${escapeHtml(blog.title)}</a></h3> <div class="blog-meta"> Tác giả: ${escapeHtml(blog.author)} • ${formatDate(blog.created_at)} </div> <div>${escapeHtml(blog.excerpt || '')}</div> </div> `; }); blogList.innerHTML = html; } function formatDate(dateString) { if (appifio_client && appifio_client.appifio_formatDateTime) { return appifio_client.appifio_formatDateTime(dateString, 'date'); } const date = new Date(dateString); return date.toLocaleDateString('vi-VN', { day: '2-digit', month: '2-digit', year: 'numeric' }); } function showError(message) { document.getElementById('loading').style.display = 'none'; const errorDiv = document.getElementById('error'); errorDiv.textContent = message; errorDiv.style.display = 'block'; } </script> </body> </html> ``` ### Example 5: Category Listing Page (blog/category.html) Similar to tag.html, but filters by category instead of tags: ```javascript // In blog/category.html, extract category slug from URL: // URL format: /urlname/category/category-slug const pathParts = window.location.pathname.split('/').filter(p => p); let categorySlug = ''; const categoryIndex = pathParts.indexOf('category'); if (categoryIndex !== -1 && pathParts.length > categoryIndex + 1) { categorySlug = pathParts[categoryIndex + 1]; } // Filter blogs by category: const categoryBlogs = Object.values(blogIndex.blogs).filter(blog => { if (!blog.category) return false; const blogCategorySlug = generateSlug(blog.category); return blogCategorySlug === categorySlug; }); ``` --- ## 📋 CRITICAL RULES ### ✅ ALWAYS DO: 1. **Initialize with default structure** before reading files 2. **Handle file not found gracefully** - it's normal for first-time 3. **Single Index File:** Store all blogs in ONE `blog.json` file 4. **Use ID as Key:** Structure as `{ blogs: { "id": {...} } }` 5. **Full Content:** Store complete blog content in `content` field 6. **Generate Slug:** Use `appifio_generateSlug()` for URL-friendly slugs 7. **Update Metadata:** Always update `metadata.total` and `metadata.last_updated` 8. **MANDATORY Routes:** Create route entry for EVERY blog, tag, and category 9. **Nested Routes:** Use `tag/slug` and `category/slug` format for nested routes 10. **Create Handler Files:** Blog detail/tag/category HTML must exist in filesystem 11. **Absolute URLs:** Always use absolute URLs for navigation (base tag issue) 12. **Remove Routes:** Always remove routes when deleting content 13. **Error Handling:** Use try-catch for all async operations 14. **Check Client:** Always check `appifio_client && appifio_client.methodName` before calling ### ❌ NEVER DO: 1. ❌ Assume file exists without checking 2. ❌ Create separate files per blog (`blog-slug.json`) 3. ❌ Store only metadata without full content 4. ❌ Hardcode IDs 5. ❌ Forget to update metadata section 6. ❌ Skip route creation (URLs won't work!) 7. ❌ Forget to create handler HTML files 8. ❌ Use relative URLs for navigation (base tag breaks them) 9. ❌ Forget to remove routes when deleting 10. ❌ Check `typeof appifio_client === 'undefined'` (it's a getter, always exists) 11. ❌ Define `class AppStorageClient` 12. ❌ Rewrite `appifio_*` methods --- ## 🔍 STORAGE SEPARATION ### TempDB (app_storage table) **Purpose:** Store data files **Use for:** `blog.json`, `settings.json`, `user-data.json` **Methods:** `appifio_readFile()`, `appifio_writeFile()` ### Filesystem (sync_data/urlname/) **Purpose:** Store HTML files and configuration **Use for:** `routes.json`, `blog/detail.html`, `index.html`, `blog/tag.html`, `blog/category.html` **Methods:** `appifio_readFsFile()`, `appifio_writeFsFile()` --- ## 🎯 COMPLETE WORKFLOW ### First-Time Setup: ``` 1. Create blog/detail.html, blog/tag.html, blog/category.html in filesystem ↓ 2. User creates first blog ↓ 3. blog.json is created (writeFile) ↓ 4. Route is added to routes.json for blog post ↓ 5. Routes are added for tags and category ↓ 6. System can now serve /urlname/blog-slug, /urlname/tag/tag-slug, /urlname/category/category-slug ``` ### Creating a Blog: ``` 1. Read blog.json (or use empty structure if not found) 2. Generate ID (timestamp) 3. Generate slug from title 4. Add new entry to blogs object 5. Update metadata 6. Save blog.json to TempDB 7. Add route to routes.json for blog post (MANDATORY) 8. Ensure routes exist for all tags (tag/tag-slug format) 9. Ensure route exists for category (category/category-slug format) 10. User can now visit /urlname/slug, /urlname/tag/tag-slug, /urlname/category/category-slug ``` ### Displaying a Blog: ``` User visits: /urlname/my-blog-slug ↓ System reads: routes.json ↓ Finds: "my-blog-slug" → "blog/detail.html" ↓ Serves: blog/detail.html ↓ blog/detail.html reads: blog.json (TempDB) ↓ Finds blog with slug: "my-blog-slug" ↓ Displays: blog content ``` ### Displaying Tag/Category: ``` User visits: /urlname/tag/javascript ↓ System reads: routes.json ↓ Finds: "tag/javascript" → "blog/tag.html" ↓ Serves: blog/tag.html ↓ blog/tag.html extracts slug "javascript" from URL ↓ Reads: blog.json (TempDB) ↓ Filters blogs by tag slug ↓ Displays: filtered blog list ``` --- ## 💡 DEBUGGING TIPS ### If blog.json not found: ```javascript // ✅ This is NORMAL for first time console.log('blog.json not found - will create on first save') ``` ### If route doesn't work: ```javascript // Check if route exists const routes = await appifio_client.appifio_getRoutes() console.log('All routes:', routes.data.routes) // Check specific route if (!routes.data.routes['my-slug']) { console.error('Route missing! Add it:') await appifio_client.appifio_addRoute('my-slug', 'blog/detail.html', 'blog') } ``` ### If nested route doesn't work: ```javascript // Check nested route const routes = await appifio_client.appifio_getRoutes() if (!routes.data.routes['tag/javascript']) { console.error('Nested route missing! Add it:') await appifio_client.appifio_addRoute('tag/javascript', 'blog/tag.html', 'tag') } ``` ### If handler file doesn't exist: ```javascript // Check if file exists const result = await appifio_client.appifio_readFsFile('blog/detail.html') if (!result.success) { console.error('Handler file missing! Create it:') await appifio_client.appifio_writeFsFile('blog/detail.html', htmlContent) } ``` ### If navigation links broken: ```javascript // Always use absolute URLs const urlName = getUrlName(); let siteUrl = window.location.origin; if (window.appifio && window.appifio.global && window.appifio.global.apps && window.appifio.global.apps.site_url) { siteUrl = window.appifio.global.apps.site_url.replace(/\/$/, ''); } const absoluteUrl = siteUrl + '/' + urlName + '/path'; ``` --- ## 🎓 FINAL REMINDER You are building an APPLICATION that USES a pre-built API. The API library is already written and injected. Your job: Write business logic using the API. Focus: User interface, data flow, user experience. Don't waste time: Reimplementing what already exists. ### Key Points: - File not found is NORMAL for first-time usage - Routes are MANDATORY for all non-index URLs - Nested routes work: `tag/slug`, `category/slug` - Handler HTML files MUST exist in filesystem - Always initialize with default structure - Always add routes when creating content - Always remove routes when deleting content - Always use absolute URLs for navigation - Always check `appifio_client && appifio_client.methodName` before calling Build great applications with the tools provided!