Changelog

All notable changes to the Avada Group Careers Website will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

Added

  • CV file upload on the application form (
    1
    
    _includes/form_ung_tuyen.html
    
    ). The applicant may now attach a PDF/DOC/DOCX instead of pasting a link; the file is base64-encoded into the submit JSON as
    1
    
    attachment: {filename, contentType, data}
    
    . Multipart is not an option — the CRM endpoint only reads
    1
    
    c.req.json()
    
    and answers 400
    1
    
    invalid_json
    
    to anything else.
    • ⚠️ Deploy order is strict: the CRM must ship first. Production currently has no
      1
      
      attachment
      
      config and still has
      1
      
      cv_url
      
      as
      1
      
      required: true
      
      . Publishing this form before the CRM deploys means a file-only application is rejected with 422 and the applicant loses it. Verify with the admin API that the live form reports an
      1
      
      attachment
      
      block and
      1
      
      cv_url required: false
      
      before pushing this repo.
    • The link field is kept beside the upload and is no longer
      1
      
      required
      
      ; at least one of the two must be present, enforced client-side. Upload failing for any reason degrades to pasting a link rather than losing the application.
    • 1
      
      contentType
      
      is derived from the filename extension, never from
      1
      
      file.type
      
      : Safari and Firefox report an empty type for
      1
      
      .doc
      
      /
      1
      
      .docx
      
      , and because
      1
      
      .docx
      
      is a zip container some browsers report
      1
      
      application/zip
      
      . The CRM matches the three accepted types exactly, so forwarding
      1
      
      file.type
      
      would reject valid CVs.
    • Size and format are checked before the file is read, so a 20MB pick fails instantly instead of after a long silent wait.
    • Base64 conversion uses
      1
      
      FileReader.readAsDataURL()
      
      rather than
      1
      
      btoa(String.fromCharCode(...new Uint8Array(buf)))
      
      , which blows the call stack on multi-megabyte files.
    • 1
      
      attachment_rejected
      
      (422) is surfaced in Vietnamese with the server’s
      1
      
      maxBytes
      
      , and keeps everything the applicant typed.
    • When the CRM answers
      1
      
      {ok: true, attachmentDropped: true}
      
      — its attachment budget is exhausted, so it kept the application but discarded the file — the applicant is told plainly, and told where to send the CV instead. This stays a success message in green: the application did reach HR, and colouring it as an error would make someone who just submitted try again, only to be blocked by deduplication.

Fixed

  • 1
    
    normalise()
    
    prefixed
    1
    
    https://
    
    unconditionally, so an empty link field became the literal string
    1
    
    https://
    
    once the field stopped being required — failing the CRM’s
    1
    
    ^https?://\S+$
    
    check and rejecting applications that correctly attached a file.

Changed

  • Applications no longer post to Slack.
    1
    
    #avada-tuyen-dung
    
    had been answering
    1
    
    channel_not_found
    
    for every submission since 24 Aug 2026 — renamed, archived, or the bot was removed — so five real applications reached HR by email but never appeared in the channel. Rather than repair the channel,
    1
    
    notify.slackChannel
    
    was dropped from the form config, because HR already works from the email and its
    1
    
    Reply-To
    
    is the applicant. A silent channel is therefore deliberate, not a regression: re-enabling Slack means fixing the channel first.
    • Nothing on this site changed; the form never knew about Slack. Recorded here because the absence is otherwise indistinguishable from the outage it replaced.
    • The underlying reporting gap is still open in the CRM:
      1
      
      status
      
      stays
      1
      
      forwarded
      
      when one channel fails and another succeeds, so neither this outage nor the August SES outage (
      1
      
      status: forwarded
      
      , email failed) surfaced anywhere.
  • Application form now submits to the Avada Forms CRM instead of EmailJS (
    1
    
    _includes/form_ung_tuyen.html
    
    )
    • 1
      
      POST https://crm-form.avada.io/v1/forms/avada-career-application/submit
      
      . The CRM stores every submission in Firestore and then fans out to Slack (
      1
      
      #avada-tuyen-dung
      
      ) and
      1
      
      [email protected]
      
      , so an application is no longer lost when the third-party mailer refuses it, and the send is no longer capped by an EmailJS quota. Form config lives in
      1
      
      avada-form-api
      
      ,
      1
      
      seeds/forms-avadagroup.json
      
      .
    • Input names stay Vietnamese (
      1
      
      ho_ten
      
      ,
      1
      
      dien_thoai
      
      , …) and are mapped to the CRM’s registered field names (
      1
      
      name
      
      ,
      1
      
      phone
      
      , …) at submit time — the CRM drops any key that does not match a registered
      1
      
      FormField.name
      
      , and prints those names verbatim into Slack and the notification email.
    • Failures are reported per CRM error code (
      1
      
      rate_limited
      
      ,
      1
      
      form_disabled
      
      ,
      1
      
      validation_failed
      
      , …) in Vietnamese;
      1
      
      validation_failed
      
      details are mapped back to the input the applicant can actually edit, and that field is focused.
    • No Cloudflare Turnstile. The form is registered with
      1
      
      requireTurnstile: false
      
      , because Avada’s single shared Turnstile site key does not allowlist the
      1
      
      avadagroup.com
      
      hostname — the widget can never render there, so a client-side token gate would block every real application while the server never asked for a token. Note that
      1
      
      localhost:4000
      
      is in the Cloudflare allowlist, so a local test would have hidden this. Re-enabling means allowlisting
      1
      
      avadagroup.com
      
      +
      1
      
      www
      
      at Cloudflare first, then
      1
      2
      
      PATCH
      {"requireTurnstile": true}
      
      on the form, and only then mounting the widget.

Added

  • Client-side bot gates mirroring the avada.io forms: an off-screen honeypot (
    1
    
    _hp_field
    
    ) and a form-open timestamp (
    1
    
    _form_ts
    
    ). Neither is forwarded to the CRM. A filled honeypot means a bot, so it gets the success message without a send. A too-fast submit only asks the person to press the button again — it must never reset the form, or a real applicant who reopens the modal and submits within two seconds loses everything they typed to a fake success message.
  • 1
    
    submission_url
    
    is sent with each application, so HR can see which job page (and which UTM campaign) produced the lead.
  • 1
    
    maxlength="2000"
    
    on the introduction textarea, matching the CRM’s cap — the server truncates a longer message silently, so the limit has to be visible while the applicant is still writing.

Removed

  • EmailJS entirely — the
    1
    
    @emailjs/browser
    
    script,
    1
    
    emailjs.init
    
    , public key
    1
    
    Nk4U0-r6ahBdk0lCj
    
    , service
    1
    
    service_9ayy3zq
    
    and template
    1
    
    template_re1z26m
    
    .
  • The hidden
    1
    
    hide
    
    input that existed only to build the EmailJS subject line.
    1
    
    vi_tri
    
    and
    1
    
    thanh_vien
    
    already carry the same two values separately, and the CRM composes the subject from them.

Fixed

  • 1
    
    ngay_ung_tuyen
    
    was stamped from
    1
    
    toISOString()
    
    , which is always UTC. Vietnam is UTC+7, so every application sent before 07:00 local time was recorded with the previous day’s date. It now uses the applicant’s local calendar date.
  • Fixed the application form silently refusing to submit (
    1
    
    _includes/form_ung_tuyen.html
    
    )
    • Root cause: native HTML constraints rejected ordinary Vietnamese input while the only feedback was an English browser tooltip that is easily missed inside the scrollable modal — pressing “Gửi ứng tuyển” appeared to do nothing at all.
    • 1
      
      ngay_sinh
      
      required zero-padded
      1
      
      dd/mm/yyyy
      
      , so
      1
      
      1/1/1995
      
      was rejected. The pattern now accepts
      1
      
      d/m/yyyy
      
      and the value is zero-padded before sending.
    • 1
      
      dien_thoai
      
      pattern was
      1
      
      ^[0-9]+$
      
      , so
      1
      
      090 123 4567
      
      and
      1
      
      +84...
      
      were rejected. Separators and a leading
      1
      
      +
      
      are now accepted and stripped before sending.
    • 1
      
      link_cv
      
      was
      1
      
      type="url"
      
      , so
      1
      
      drive.google.com/...
      
      without a scheme was rejected.
      1
      
      https://
      
      is now prepended automatically.
    • Validation messages are now in Vietnamese, shown in an in-form status area, and the first invalid field is scrolled into view.
  • Submit failures now surface the real EmailJS status and message instead of a generic “Lỗi khi gửi email”, so quota/config failures are diagnosable.
  • Birth date is now checked for being a real, plausible date (the relaxed pattern alone would accept
    1
    
    31/02/1995
    
    ).

Changed

  • Application form “Ứng tuyển ngay” trigger changed from a bare
    1
    
    <a>
    
    (no
    1
    
    href
    
    , so it was not keyboard focusable) to a
    1
    
    <button type="button">
    
    ; the modal now closes on
    1
    
    Escape
    
    and restores focus to the trigger.
  • Modal wiring uses
    1
    
    querySelectorAll
    
    , so every “Ứng tuyển ngay” button on a page works rather than only the first one.
  • Email field changed from
    1
    
    type="text"
    
    to
    1
    
    type="email"
    
    — previously
    1
    
    abc
    
    passed as a valid address.
  • Submit button is disabled and relabelled “Đang gửi…” while a submission is in flight.
  • Added
    1
    
    autocomplete
    
    attributes and
    1
    
    aria
    
    roles to the modal and its fields.

Removed

  • 1
    
    validateForm()
    
    — dead code. The HTML
    1
    
    required
    
    attributes block submission before it could ever run, so its alert branch was unreachable.

Added

  • CLAUDE.md documentation for repository context and development guidelines
  • CHANGELOG.md to track project changes
  • ROADMAP.md for future feature planning

Note on the Google Forms migration

An earlier entry in this file claimed the custom form had been replaced by an embedded Google Forms iframe. That change was reverted on the same day and never shipped. The live application form is, and has remained, the custom form backed by EmailJS. History:

Commit Change
1
a031934
Replace custom application form with Google Forms iframe
1
d34dab4
Fix double scrollbar issue in Google Forms modal
1
ba66ee2
Revert to custom EmailJS application form
1
0ba4114
Revert EmailJS configuration to original service and template
1
19b68a2
Update EmailJS public key to match new account

1
_jobs/CTV-Content-SEO.md
is the one job that does link out to a standalone Google Form; it is a separate flow and is not the site-wide application form.

[2024-Present] - Ongoing Maintenance

Features

  • Job posting system with Jekyll collections
  • Application form with EmailJS integration
  • Department-based job filtering (Technical, Marketing, Sales, Service, Back Office)
  • Keyword search functionality via JSON API
  • Job badge system (HOT and TUYỂN GẤP badges)
  • Blog/news section for company updates and internal events
  • Responsive design with Bootstrap framework

Integrations

  • EmailJS for email delivery (service
    1
    
    service_9ayy3zq
    
    , template
    1
    
    template_re1z26m
    
    )
  • Google Analytics (UA-76130628-7)
  • Disqus comments (shortname: careermageplaza)

Google Apps Script / Google Sheets logging was removed in

1
7823d79
because of a CORS error and is no longer part of the site. Applications arrive by email only.


Template

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
## [Version] - YYYY-MM-DD

### Added
- New features

### Changed
- Changes in existing functionality

### Deprecated
- Soon-to-be removed features

### Removed
- Removed features

### Fixed
- Bug fixes

### Security
- Security improvements