Andrey Yasko 918f76806e Neo: loading keeps custom children mounted; loadingLabel is a string vor 6 Stunden
..
.agents 6ac00909c8 Neo: rewrite agent docs for the UNA repo, drop generic skills (#5789) vor 1 Woche
.claude 6ac00909c8 Neo: rewrite agent docs for the UNA repo, drop generic skills (#5789) vor 1 Woche
apps 5ac43ff822 Neo: manifest icons load from /static/, Vercel scripts only on Vercel (#5934) vor 7 Stunden
crypto-shim ee96f108ce Merge commit '1fd760c09fc65eb1cec5ab741bc90a0b0dfb9771' as 'neo' vor 2 Wochen
packages 918f76806e Neo: loading keeps custom children mounted; loadingLabel is a string vor 6 Stunden
patches 4ed6194c14 Neo: update Next.js to 16.4.0 and Expo SDK 57 packages (#5790) vor 1 Woche
scripts ae9bd4bb78 Home "Add new..." block: a card on the site again, solid on phones in NEO (#5915) vor 1 Tag
.easignore 6ac00909c8 Neo: rewrite agent docs for the UNA repo, drop generic skills (#5789) vor 1 Woche
.gitattributes ee96f108ce Merge commit '1fd760c09fc65eb1cec5ab741bc90a0b0dfb9771' as 'neo' vor 2 Wochen
.gitignore ee96f108ce Merge commit '1fd760c09fc65eb1cec5ab741bc90a0b0dfb9771' as 'neo' vor 2 Wochen
.prettierrc.json ee96f108ce Merge commit '1fd760c09fc65eb1cec5ab741bc90a0b0dfb9771' as 'neo' vor 2 Wochen
AGENTS.md 367b064835 Neo: neutral fills are translucent ink that stacks (#5923) vor 1 Tag
CLAUDE.md 6ac00909c8 Neo: rewrite agent docs for the UNA repo, drop generic skills (#5789) vor 1 Woche
Dockerfile ee96f108ce Merge commit '1fd760c09fc65eb1cec5ab741bc90a0b0dfb9771' as 'neo' vor 2 Wochen
LICENSE ee96f108ce Merge commit '1fd760c09fc65eb1cec5ab741bc90a0b0dfb9771' as 'neo' vor 2 Wochen
README.md a3607cfcc8 Analytics: web and product analytics in Studio, on the site and in NEO (#5856) vor 1 Woche
animated-icons.md 6ac00909c8 Neo: rewrite agent docs for the UNA repo, drop generic skills (#5789) vor 1 Woche
doctor.config.json ee96f108ce Merge commit '1fd760c09fc65eb1cec5ab741bc90a0b0dfb9771' as 'neo' vor 2 Wochen
eslint.config.mjs ee96f108ce Merge commit '1fd760c09fc65eb1cec5ab741bc90a0b0dfb9771' as 'neo' vor 2 Wochen
package.json a59ff1492f Interface sounds from Cuelume, with a Sounds section in Studio settings (#5855) vor 1 Woche
tsconfig.json ee96f108ce Merge commit '1fd760c09fc65eb1cec5ab741bc90a0b0dfb9771' as 'neo' vor 2 Wochen
turbo.json a3607cfcc8 Analytics: web and product analytics in Studio, on the site and in NEO (#5856) vor 1 Woche

README.md

NEO

NEO is the web and native client for UNA CMS. One codebase in packages/app runs as a Next.js site (apps/next) and as an Expo app for iOS and Android (apps/expo). UNA describes the pages and their blocks through its API, and NEO renders them on every platform.

  • Contributing to NEO: NEO is developed in the UNA repository at neo/. Branch from master there and open a pull request against unacms/UNA. unacms/neo is a read-only mirror of that folder; don't open pull requests on it.
  • Building a client project: fork unacms/neo, change only packages/app/customization/, and sync your fork from unacms/neo main to take updates.

AGENTS.md is the guide for both, for people and coding agents: the rules for forks, every customization seam, the architecture and the code conventions. This README covers setup and deployment.

Repository layout

apps/next        Next.js web app
apps/expo        Expo app for iOS and Android
packages/app     shared code (design system, components, UNA client, settings)
  customization/ the only folder a fork changes
scripts/         typecheck, customization merge and translation checks
patches/         patch-package patches, applied on install

Stack: Next.js 16 (webpack), React 19.2, Expo SDK 57 with React Native 0.86, TypeScript 6, Tailwind CSS 4 on web and Uniwind on native, Node 24 and Yarn 1 workspaces. package.json files hold the exact versions.

Setup

Requirements:

  • Node.js 24 and Yarn 1.22 (use Yarn, not npm).
  • For iOS: macOS with Xcode. For Android: Android Studio with an emulator or a device. Use the Xcode and Android SDK versions that Expo SDK 57 supports.
  • A UNA site. In UNA Studio → API, add an API key and the origins NEO uses (the web app URL, and APP_ORIGIN for the native app).

    yarn            # from the NEO root (neo/ in the UNA repo)
    

yarn.lock is not committed, so yarn resolves the version ranges in package.json afresh. Keep your local yarn.lock between installs, and don't edit dependency ranges unless you mean to upgrade.

Create .env.local in the NEO root:

UNA_URL=https://una.example.com
NEXT_PUBLIC_UNA_URL=https://una.example.com
UNA_API_KEY=key-from-una-studio
APP_URL=https://app.example.com
NEXT_PUBLIC_APP_URL=https://app.example.com
APP_ORIGIN=neo://app
Variable Used for
UNA_URL, NEXT_PUBLIC_UNA_URL The UNA site. On web, NEXT_PUBLIC_UNA_URL wins.
UNA_API_KEY UNA API key. Server only: the web proxy and page shell send it. Never ship it in the native app.
APP_URL, NEXT_PUBLIC_APP_URL The NEO web app's own URL.
APP_ORIGIN The Origin header the native app sends to UNA; UNA must allow it.
UNA_PREVIEW_DOMAIN, NEXT_PUBLIC_UNA_PREVIEW_DOMAIN, UNA_PREVIEW_API_KEY Optional. Routes pr-<n>.<domain> to a preview UNA backend.
GOOGLE_WEB_CLIENT_ID Optional. Google sign-in. Google sign-in doesn't work on .localhost URLs.
GOOGLE_MAPS_API_KEY, NEXT_PUBLIC_GOOGLE_MAPS_API_KEY Optional. Maps.
ONE_SIGNAL_API_KEY Optional. Push notifications.
RNMAPBOX_MAPS_DOWNLOAD_TOKEN Optional. Downloads the Mapbox native SDK at build time.
EAS_PROJECT_ID Optional. EAS builds and updates.
REDIS_URL Multi-tenant mode only: the domain map.
PORT, HOST, PROTO, HTTPS Self-hosted web server (apps/next/pm2.config.js).
NEO_EDGE Optional. What sits in front of the web server, so the proxy passes UNA only the visitor address and location headers it set: vercel (the default on Vercel), cloudflare, cloudfront, proxy (the default: a reverse proxy or load balancer that appends to X-Forwarded-For, such as nginx with $proxy_add_x_forwarded_for, Caddy or Railway) or none (browsers reach next start directly).
NEO_IMAGES_ALLOW_LOCAL_IP Local development only. 1 lets next/image load images from a UNA on your machine or network; see below.

Next.js refuses to optimize an image whose host resolves to a private IP address (127.0.0.1, ::1, 10.x, 192.168.x, ...). Against a local UNA (UNA_URL=http://localhost:8088), the dev server then logs "upstream image ... hostname resolved to private IP" and /_next/image answers 400 for every UNA image. Set NEO_IMAGES_ALLOW_LOCAL_IP=1 in .env.local and restart the dev server; it turns on Next's images.dangerouslyAllowLocalIP.

Never set it on a deployed server. The check stops server-side request forgery (SSRF): /_next/image fetches any URL on the image allowlist, and the allowlist includes localhost on every port. With the flag on, anyone can make the server request its own internal services through the image optimizer.

The native app resizes images through the optimizer at config.native_app_images_url, a deployed NEO site by default. That site can't reach your local UNA, so the request fails and the app loads UNA's image URL directly. If you point native_app_images_url at your local web dev server, the same flag applies there.

The web build on Vercel goes through Turborepo, which hides variables that turbo.json globalEnv doesn't list. Add new variables there.

Commands

Run every command from the NEO root. The scripts load .env.local; running next or expo directly doesn't.

Command What it does
yarn web2 Next.js dev server on port 3000 (yarn web2 -p 3001 for another port).
yarn web The same through portless at neo.localhost (needs the global portless CLI).
yarn native Metro for the native app, with a cleared cache.
yarn ios --device <udid or name> Builds the iOS app, installs it on a simulator and starts Metro.
yarn ios:device <udid> The same on a physical iPhone.
yarn android Builds and installs the Android app. Add --no-bundler when Metro already runs.
yarn build Production build of the web app.
yarn prod2, yarn prod, yarn prod:pm2 Serve the production web build: plain, through portless, or with PM2.
yarn analyze Web bundle analysis.
yarn typecheck TypeScript check of web and native (yarn typecheck web for one platform).
yarn lint ESLint, including the React Compiler rules.
yarn clean Deletes every node_modules.

apps/expo/ios and apps/expo/android are generated (expo prebuild) and not committed. The first yarn ios or yarn android creates them. After you change apps/expo/app.config.js, a config plugin, fonts or a native dependency, run the build again so prebuild picks up the change. The debug app loads its JavaScript from Metro on port 8081.

Deployment

Web on Vercel

Create a Vercel project from your repository with Root Directory apps/next, and set the environment variables above in the project settings.

Web on your own server

yarn build
yarn prod2          # or: yarn prod:pm2

yarn prod:pm2 starts the neoapp PM2 process from apps/next/pm2.config.js: 2 instances with a 250 MB memory limit, or 4 instances with 1 GB in the production profile. The Dockerfile builds an image the same way; it needs a yarn.lock in the build context.

Native apps

Build store and test builds with EAS Build. The repository has no eas.json; a fork adds apps/expo/eas.json with its own profiles, and sets the environment variables in EAS, because .env.local isn't uploaded. .easignore makes EAS upload your local yarn.lock, so the cloud build installs the versions you tested.

cd apps/expo
eas build --profile production --platform ios
eas build --profile production --platform android

Troubleshooting

Problem Try
Module not found after pulling yarn, then restart Metro (yarn native) or the dev server.
Native app shows an old screen or a red box about a native module The native code changed: rebuild with yarn ios / yarn android.
Stale web build Delete apps/next/.next and restart.
Type errors yarn typecheck. The Next build ignores type errors, so this is the only check.

License

See LICENSE.