Search by

nawasara / tourism

pringgojsnawasara

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.

Package info

github.com/nawasara/tourism

pkg:composer/nawasara/tourism

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.2.0 2026-09-14 05:21 UTC

This package is auto-updated.

Last update: 2026-09-14 05:33:48 UTC


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. VisitPonorogoClient parses it with DOMDocument and XPath, not regex: the site markup is full of irregular whitespace and a two-column layout (.col-lg-3 for the title, .col-lg-9 for 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_published belongs 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 is external_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.