# About & Contact — one page (Main · Public site)

**Portal:** Main · **Route:** `GET /about` (`main.site.about`) → `Main\AboutController@index` →
`Pages/Main/Site/About.vue` · **Nav:** **ONE** tab in the site header (**About**). The FOOTER's Company list still names
About and Contact separately, the second deep-linking to `/about#contact` — a header is a set of
destinations, a footer is a directory, and someone scanning a footer for the word "Contact" should
find the word "Contact" · **Gated by:** NOTHING, it is **PUBLIC**
(`public.site`, so it gets the site chrome, the live country list and the translation catalog) ·
**Writes:** none, it is a read-only page.

## What it does

Answers *who are you* and *how do I reach you* in one scroll, because the same visitor asks both in
the same breath and neither answer fills a page on its own.

**The order is the buyer's question, not a company history.** A new-launch buyer arrives asking
*whose side are you on, and what does going through you cost me* — so:

1. **Hero** — the claim, two buttons (Talk to us → `#contact`, See the memberships), and two REAL
   figures: the catalogue's project count for this hostname's markets, and the markets themselves
   as links.
2. **What we do** — three cards: screening with numbers · teaching judgement · AI in the decision.
3. **Five checks** — the screening method, as five NUMBERED cards. It used to be one long sentence
   inside a paragraph; a reader counts what is numbered and skims what is not, and the count *is*
   the claim.
4. **Who pays whom** — the loudest band on the page, and the only dark one, because this objection
   is the one every other sentence has to get past: *the agent's commission is paid by the
   developer, so the price on your S&P is the developer's price.* Two panels underneath say who is
   paid by whom.
5. **Contact** (`#contact`) — WhatsApp as the one full-width primary card, every other channel and
   the member sign-in flowing in two columns beneath it, then the legal / postal block. Only
   WhatsApp spans: a second full-width card leaves a hole beside whatever precedes it whenever the
   channel count is odd, and that count changes with config.

**SEO.** The page emits `Seo::make()` with a canonical and an **Organization** JSON-LD node,
enriched in `organizationSchema()` with `legalName` / `email` / `address` / `contactPoint` — but,
like the visible page, only for the fields that are actually configured. A search engine reads this
page for who the company is; guessing there is the same defect as guessing on screen.

## How it works

**Nothing on this page is invented, and nothing is a placeholder.** Every fact is read from config
that already existed, or from config created for it:

| Shown | Comes from |
|---|---|
| WhatsApp, LINE, WeChat | `config/services.php` → `propertylab.sales_whatsapp`, `propertylab.line_url`, `hk_lead.wechat_id/qr` — the SAME values the project pages offer, never a second copy |
| Legal name, company no., address, email | `config/company.php` — transcribed **verbatim from https://propertylab.tech/contact** on 2026-09-08 at the owner's instruction. They are config DEFAULTS, not env-only, because the company is the same company on every deployment; the env overrides remain |
| Opening hours | nothing publishes them, so the field is empty and the block hides itself |
| Active markets | the `site.countries` shared prop (`SetPublicSite`) |
| Projects analysed | `CatalogProject` counted over those countries — the same figure the home page's hero counts |

⚠️ **An unfilled field is an ABSENT BLOCK, not a placeholder.** `AboutController::company()` drops
every empty value and the page asks one `v-if` per block, so an unset field renders as no section at
all, and is also left out of the JSON-LD. A made-up postal address on a public page is worse than no
address, and a visible `{{ TODO }}` is a made-up address that admits it. Override any of them per
deployment without a code change:

```
COMPANY_LEGAL_NAME=      COMPANY_REGISTRATION_NO=
COMPANY_ADDRESS_LINE1=   COMPANY_ADDRESS_LINE2=
COMPANY_ADDRESS_CITY=    COMPANY_ADDRESS_COUNTRY=
COMPANY_EMAIL=           COMPANY_HOURS=
```

⚠️ **`hello@propertylab.tech` is the AGENCY inbox.** The source page labels it *"For pilots &
partnerships"*, and this is the buyer-facing site. It is listed because it is the only email the
company publishes; point `COMPANY_EMAIL` somewhere else the moment buyer mail should land
somewhere else — the contact card, the legal block and the JSON-LD all follow that one value.

⚠️ **After changing `config/company.php`, run `php artisan config:cache`** — this box caches config.
No asset build is needed: the page reads props, so the values change without touching the bundle.

**The WeChat QR appears only when the asset really exists** (`File::exists(public_path(...))`) —
`public/images/wechat-lead-qr.png` is currently **missing**, which is why the WeChat card shows the
ID and no code. Drop the file in and the code appears; no code change.

**The phone is formatted once, in the controller** (`60122734886` → `+60 12-273 4886`), so the page
never re-formats a number or rebuilds a `wa.me` URL.

**Language:** every string is an English translation key, like the rest of the public site — the
Chinese lives in `resources/lang/zh_CN.json` and `zh_HK.json`. See the
[pricing page's Language section](/docs/modules_handbook/main/membership-plans/readMe.md) for the
rules; the zh_HK values are produced with OpenCC **`s2hk`**, the config that matches that file's
existing content **780/780**. Check any conversion of `复` by hand — `案例复盘` converts to
`案例覆盤`, which is wrong; it is `案例復盤`.

⚠️ **Check a short key against the catalog before adding one.** `Developer` was already
`开发商 / 開發商`, and this product's own reviewed copy says **发展商** everywhere — the five-check
card is `Track record` for that reason. `Handover` (`入伙 / 入夥`, the Hong Kong pages) is the same
trap on the pricing page.

## Related files

- `app/Http/Controllers/Main/AboutController.php`
- `config/company.php` — legal / postal identity, empty by default
- `resources/js/Pages/Main/Site/About.vue`
- `resources/js/Layouts/SiteLayout.vue` — the header + footer entries that reach it
- `routes/main.php` — `GET about`, PUBLIC, declared **before** the trailing `{slug}` funnel
  catch-all that would otherwise claim a one-segment path. **Route cache:** run
  `php artisan route:cache` after touching it — this box caches routes.
- `resources/lang/zh_CN.json` · `zh_HK.json`

## Related modules

- [Membership plans](/docs/modules_handbook/main/membership-plans/readMe.md) — the page the
  "Who does what" band links to.
