func renderPage(id string, u *user.User, c context.Context, w http.ResponseWriter, r *http.Request, edit bool) {

	var pageKey *datastore.Key

	pID, err := strconv.ParseInt(id, 10, 64)
	if err != nil {
		// check alias
		paKey := model.NewPageAliasKey(c, id)
		var pa model.PageAlias
		if err := ds.Get(c, paKey, &pa); err != nil {
			handleError(c, w, err, http.StatusNotFound)
			return
		}
		pID = pa.PageKey.IntID()
		pageKey = pa.PageKey
	} else {
		pageKey = model.NewPageKey(c, pID)
	}

	var p model.Page
	if err := ds.Get(c, pageKey, &p); err != nil {
		handleError(c, w, err, http.StatusNotFound)
		return
	}

	if edit {
		if u == nil {
			loginURL, _ := user.LoginURL(c, p.URLBase()+common.EDIT_PAGE_EXT)
			http.Redirect(w, r, loginURL, http.StatusFound)
			return
		} else if !u.Admin {
			// TODO: prepare error page
			http.Redirect(w, r, "/caterpillar/error/invalidUser", http.StatusFound)
			return
		}
	}

	leaf := leaves[p.Leaf]
	if leaf == nil {
		errmsg := fmt.Sprintf("leaf not found:" + p.Leaf)
		handleError(c, w, errors.New(errmsg), http.StatusNotFound)
		return
	}

	tparam := struct {
		Properties map[string]interface{}
		Areas      map[string]interface{}
		User       *user.User
		Edit       bool
		PageID     int64
		ViewURL    string
		EditURL    string
		PagesURL   string
		LogoutURL  string
	}{
		make(map[string]interface{}),
		make(map[string]interface{}),
		u,
		edit,
		pID,
		"",
		"",
		"",
		"",
	}

	if u != nil {
		tparam.PagesURL = "/caterpillar/static/mng/#/queryPage"

		purl := p.URLBase()
		tparam.ViewURL = purl + common.VIEW_PAGE_EXT
		tparam.EditURL = purl + common.EDIT_PAGE_EXT

		logoutURL, err := user.LogoutURL(c, tparam.ViewURL)
		if err != nil {
			// only log
			applog.Warningf(c, "cannot get logoutURL. err:%v", err)
		}
		tparam.LogoutURL = logoutURL
	}

	futureProps := make(map[string]<-chan func() (*model.PageProperty, error))
	futureAreas := make(map[string]<-chan func() (*model.Area, []model.Block, error))

	for _, hole := range leaf.Wormholes {
		var pkey *datastore.Key
		if hole.Global {
			pkey = model.NewGlobalPageKey(c)
		} else {
			pkey = pageKey
		}
		switch hole.Type {
		case PROPERTY:
			propkey := model.NewPagePropertyKey(c, hole.Name, pkey)
			ch := getPagePropertyAsync(c, propkey)
			futureProps[hole.Name] = ch
		case AREA:
			ch := getAreaAndBlocksAsync(c, pkey, hole.Name)
			futureAreas[hole.Name] = ch
		}
	}

	pageURLs := make(map[string]string)

	for _, hole := range leaf.Wormholes {
		switch hole.Type {
		case PROPERTY:
			prop, err := (<-futureProps[hole.Name])()
			if err == nil {
				tparam.Properties[hole.Name] = prop.Value
			} else {
				// TODO handle error
				applog.Errorf(c, "%s", err)
			}
		case AREA:
			area, blocks, err := (<-futureAreas[hole.Name])()

			if err == nil {
				areasrc := renderArea(hole.Global, area, blocks, edit)

				if !edit {
					futurePages := make(map[string]<-chan func() (*model.Page, error))

					urls := pageUrlRegex.FindAllStringSubmatch(areasrc, -1)

					for _, url := range urls {
						purl := url[0]

						if _, exists := pageURLs[purl]; exists {
							continue
						}
						if _, exists := futurePages[purl]; exists {
							continue
						}

						pageID, err := strconv.ParseInt(url[1], 10, 64)
						if err != nil {
							// TODO handle error
							applog.Errorf(c, "%s", err)
							continue
						}

						pkey := model.NewPageKey(c, pageID)
						futurePages[purl] = getPageAsync(c, pkey)
					}

					for purl, ch := range futurePages {
						page, err := (<-ch)()
						if err != nil {
							// TODO handle error
							applog.Errorf(c, "%s", err)
							continue
						}
						pageURLs[purl] = page.URLBase() + common.VIEW_PAGE_EXT
					}

					areasrc = pageUrlRegex.ReplaceAllStringFunc(areasrc, func(s string) string {
						if r, true := pageURLs[s]; true {
							return r
						}
						return s
					})
				}

				tparam.Areas[hole.Name] = template.HTML(areasrc)
			} else {
				// TODO handle error
				applog.Errorf(c, "%s", err)
			}
		}
	}

	// TODO: validate reserved page name property
	// or put some prefix?
	tparam.Properties["name"] = p.Name

	// TODO: resolve charset from somewhere
	w.Header().Set("Content-Type", "text/html; charset=utf-8")

	if err = leaf.Template.Execute(w, tparam); err != nil {
		handleError(c, w, err, http.StatusInternalServerError)
		return
	}

	return

}