Routes table showing absolute visitor URLs for each path
Absolute visitor URLs

Appifio Creator · Lesson AM10 · URL · Empty data · Rate limit

Three easy-to-mix topics
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 on every app page. Relative URLs resolve from the app root, not from a template folder like blog/detail.html.

Visitor: https://domain.com/{url_name}/{route_path}
→ PHP reads routes.json → serves HTML template
→ Head has 

site_url in JS is always the platform domain - it can differ in host from when the app uses a custom domain. Prefer the origin from the tag.

How you write itResult
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:

LayerWhen missingExample
API data store (JSON)success: true + empty - not 404readListcontent: []
Physical FS (HTML on disk)success: false if not created yetreadFsFile('blog/detail.html')
Visitor URL (routing)HTTP 404 pageSlug missing from routes.json

B.1 - First data-store reads

MethodWhen there is no data yet
readListcontent: []
readFile (flat)content: ""
readFile (indexed + id)content: null (not found / filtered)
✅ Correct pattern
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
}
❌ Wrong pattern
// 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

Backend panel showing monthly API request usage limits
API rate limits

Limits are counted per IP + link (url_name) - each app is independent.

TypeCounted?When exceeded
Mutation (append, update, write, upload, route, password change…)✅ High in a short windowTemporary IP block
executeBackendFunction✅ Separate - longer windowTemporary IP block
Login with wrong password✅ Low thresholdTemporary 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:

  1. Redirects / links use getAbsoluteUrl, not a leading /?
  2. Empty list = [], not treated as an error?
  3. Publish uses addRoute; delete uses removeRoute?
  4. Public forms have try/catch + err.blocked?

Appifio Creator · Method series · AM10