nawasara / tourism
Tourism directory for the Nawasara superapp framework — mirrors Ponorogo's Visit Ponorogo destinations (scraped from disbudparpora HTML) and serves them over a public, no-auth read API for mobile clients.
Requires
- php: ^8.1
- illuminate/support: ^10.0|^12.0
- livewire/livewire: ^3.0
- nawasara/core: *
- nawasara/sync: *
- nawasara/ui: *
- spatie/laravel-permission: ^6.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A tourism destination directory for the Nawasara superapp framework. It mirrors the Visit Ponorogo data (run by disbudparpora) into the Nawasara database, then serves it through a public, no-login API for the citizen app (the Flutter SuperApps): list, detail, and map points.
Status v0.2.0
| Feature | Status |
|---|---|
| Sync destination list (name, image, rating) | ready |
| Sync detail (description, address, opening hours, facilities, price, coordinates) | ready |
| Public API: list + detail + map filter | ready |
| Admin panel: list, detail, publish/hide, manual sync | ready |
| Map view in the panel | not built yet, will be merged with CCTV and WiFi |
| Categories (nature/culinary/religious/etc.) | ready (v0.2.0) |
Setup
composer require nawasara/tourism php artisan migrate php artisan db:seed --class="Nawasara\\Tourism\\Database\\Seeders\\PermissionSeeder" php artisan tinker --execute="Nawasara\Tourism\Jobs\SyncDestinationsJob::dispatch();"
Register it in resources/css/app.css:
@source "../../vendor/nawasara/tourism";
Optional configuration via .env:
NAWASARA_TOURISM_BASE_URL=https://disbudparpora.ponorogo.go.id/visitponorogo
NAWASARA_TOURISM_SYNC_INTERVAL=1440 # minutes, once a day
NAWASARA_TOURISM_DETAIL_DELAY_MS=300 # delay between detail pages
NAWASARA_TOURISM_RATE_LIMIT_PER_MINUTE=300
Endpoints (public, no login)
| Method | Path | Use |
|---|---|---|
| GET | {api-prefix}/tourism/destinations |
List; ?search=, ?category=, ?mappable=1, ?per_page= |
| GET | {api-prefix}/tourism/destinations/{externalId} |
Detail for one destination |
| GET | {api-prefix}/tourism/categories |
Category list with a count per category |
externalId is the destination id on the source site (…/destinasi_detail/{id}), the public key the app holds. The API returns a category on each destination as both a category slug and a category_label.
Permissions
| Permission | For |
|---|---|
tourism.destination.view |
View list and detail in the panel |
tourism.destination.update |
Publish or hide a destination |
tourism.destination.sync |
Run a sync from the panel |
Design notes
-
We scrape HTML rather than call an API, because the source has no API. Visit Ponorogo only renders HTML.
VisitPonorogoClientparses it with DOMDocument and XPath, not regex: the site markup is full of irregular whitespace and a two-column layout (.col-lg-3for the title,.col-lg-9for the body), which makes regex break silently. The HTML shape is pinned by parser tests (tests/, with real HTML fixtures as of 12 September 2026) so that a shift in the source shows up here, not in the app. -
Sync accumulates, it does not mirror. Destinations that disappear from the source are not deleted (the source site occasionally has errors or gets reworked). Deletion is a human task through the panel, not a side effect of sync, same as nawasara/news.
-
Detail is supplementary, and a failure to load it does not fail the whole sync. If one detail page fails to open, the row is still saved with the list-card data (name plus image is enough to display); the description arrives on the next sync. One broken page must not fail the other 71.
-
is_publishedbelongs to Nawasara, not the source. Sync never touches it, so a staff member who hides a wrong destination will not see it come back after the next sync. -
Price and distance are stored raw ("Rp.-", "24 km") and normalized in the presenter. The source format is not always consistent; storing it as-is means the interpretation ("free" when there is no number) can change without a migration, and the original value stays visible if that interpretation turns out to be wrong.
-
Primary key is bigint plus a unique
external_id, not UUID. The rows are not referenced by auto-increment id from outside; the public key isexternal_id(the source site id). The Resource never leaks the internal id (AGENTS.md §10a). -
Per-IP rate limit (300/min), separate from the token limit. Public routes without login are counted per IP, and mobile citizens share a carrier NAT IP, so 60/min would punish citizens who did nothing. See the same long note in nawasara/news.
-
Categories come from the category pages, not the detail page. The detail markup carries no category label, but the source lists destinations under five category pages at /site/destinasi/{1..5}. The sync opens those five pages once and records which destinations appear on each, so a destination's category is known from where it is listed, not guessed from its name. Checked 14 September 2026: the per-category counts (13, 31, 7, 13, 8) sum to exactly 72, the total number of destinations, so every destination falls into exactly one category.
Roadmap
- Map view: destinations with coordinates (
scopeMappable) plotted together with CCTV and WiFi points (both also have lat/lng) on one map.
Author
Pringgo J. Saputro, Kominfo Ponorogo. MIT License.