
Appifio Creator · Lesson AM10 · URL · Empty data · Rate limit
links · empty results · API limits
Visitor URLs differ from the data-store API. First reads are empty - not errors. Dense mutations can temporarily block an IP.
Learning goals: Link on the right domain; treat content: [] as normal; always addRoute when publishing; wrap try/catch for rate limits.
Previous: AM09 · Next: AM11 (System files & CMS)
A. Absolute URLs - why matters
The platform injects blog/detail.html.
→ PHP reads routes.json → serves HTML template
→ Head has
site_url in JS is always the platform domain - it can differ in host from
| How you write it | Result |
|---|---|
href="tag/js" | ✅ Correct - slug under the app root |
getAbsoluteUrl('tag/js') | ✅ Explicit - use for location.href, JS-built links |
href="/tag/js" | ❌ Domain root - drops url_name |
href="../tag/js" | ❌ .. leaves the app |
href="blog/detail.html" | ❌ FS template path - visitors need a route slug, not .html |
Rule: Navigation links - getAbsoluteUrl(slug) or href="slug" (no leading /, no .., no .html). The slug must exist in routes.json (addRoute).
Helper - paste into app HTML
/** Prefer- works with custom domains too */ function getAppBaseUrl() { const base = document.querySelector('base[href]')?.href if (base) return base.replace(/\/$/, '') + '/' const site = (window.appifio?.global?.apps?.site_url || window.location.origin).replace(/\/$/, '') const name = window.appifio?.global?.apps?.url_name || '' return name ? `${site}/${name}/` : `${site}/` } /** path = route slug: '' | 'admin' | 'my-post' | 'tag/javascript' */ function getAbsoluteUrl(path) { const base = getAppBaseUrl().replace(/\/$/, '') const segment = String(path || '').replace(/^\//, '') return segment ? `${base}/${segment}` : `${base}/` }
Example (url_name=test-blog) | Result |
|---|---|
getAbsoluteUrl('') | App home |
getAbsoluteUrl('admin') | Admin panel |
getAbsoluteUrl('login') | Login page |
getAbsoluteUrl('an-article-about-ai') | Post detail (route slug) |
getAbsoluteUrl('tag/javascript') | Nested tag page |
B. Empty data & required routes
Two separate ideas - do not mix them:
| Layer | When missing | Example |
|---|---|---|
| API data store (JSON) | success: true + empty - not 404 | readList → content: [] |
| Physical FS (HTML on disk) | success: false if not created yet | readFsFile('blog/detail.html') |
| Visitor URL (routing) | HTTP 404 page | Slug missing from routes.json |
B.1 - First data-store reads
| Method | When there is no data yet |
|---|---|
readList | content: [] |
readFile (flat) | content: "" |
readFile (indexed + id) | content: null (not found / filtered) |
async function loadBlogs() {
const result = await appifio_client.appifio_readList('blog.json', ['id','title','slug'])
if (result?.success) return result.data.content || [] // [] first time - normal
return []
}
// First post - backend creates the ledger
await appifio_client.appifio_appendData('blog.json', { title, slug, content })
// Flat config - keep defaults if empty
let config = { theme: 'light' }
const res = await appifio_client.appifio_readFile('config.json')
if (res?.success && res.data.content && typeof res.data.content === 'object') {
config = res.data.content
}// Expect failure when the file is missing - data store does NOT do that
if (!result.success) { /* never on first read */ }
// JSON.parse - Aura already parsed
const items = JSON.parse(result.data.content)
// writeFile for a list - breaks indexed sharding
await appifio_client.appifio_writeFile('blog.json', JSON.stringify([...]))B.2 - Routes required for visitor URLs
By default only /urlname/ serves index.html. Every other slug must have an entry in routes.json - otherwise visitors get a 404 (unrelated to the data-store API).
// ❌ WRONG - data exists but visitor gets 404
await appifio_client.appifio_appendData('blog.json', blogData)
// ✅ RIGHT - data + route + HTML template already on FS
const { data: { id } } = await appifio_client.appifio_appendData('blog.json', blogData)
await appifio_client.appifio_addRoute(blogData.slug, 'blog/detail.html', 'blog')
// Nested tag / category routes
await appifio_client.appifio_addRoute('tag/javascript', 'blog/tag.html', 'tag')
await appifio_client.appifio_addRoute('category/tech', 'blog/category.html', 'category')
// Delete post → remove matching route
await appifio_client.appifio_updateData('blog.json', id, null)
await appifio_client.appifio_removeRoute(slug)addRoute maps slug → HTML file on the physical FS - blog/detail.html must exist (Panel or writeFsFile). Method detail: AM05.
C. Rate limit & IP block

Limits are counted per IP + link (url_name) - each app is independent.
| Type | Counted? | When exceeded |
|---|---|---|
| Mutation (append, update, write, upload, route, password change…) | ✅ High in a short window | Temporary IP block |
executeBackendFunction | ✅ Separate - longer window | Temporary IP block |
| Login with wrong password | ✅ Low threshold | Temporary IP block |
| Read (readList, getRoutes, getSession…) | ❌ Not limited | - |
logout | ❌ Not counted as mutation quota | - |
When an IP is blocked: HTTP 429, client maps err.blocked === true. You do not get remaining time or a detailed reason - UI should say a generic “Try again later”.
try {
await appifio_client.appifio_appendData('contacts.json', data)
} catch (err) {
if (err.blocked) {
alert('IP temporarily blocked due to too many API calls. Please try again later.')
return
}
if (err.httpStatus === 401) { /* session expired - go to login */ }
throw err
}AM10 checklist:
- Redirects / links use
getAbsoluteUrl, not a leading/? - Empty list =
[], not treated as an error? - Publish uses
addRoute; delete usesremoveRoute? - Public forms have
try/catch+err.blocked?
Internal navigation (same language)
Appifio Creator · Method series · AM10