WIT LAB INC Blog

β€œ To Impress Others By Works"

Automating Daily Vocabulary Posts with Laravel, Structured AI, and the Facebook Graph API

Automating Daily Vocabulary Posts with Laravel, Structured AI, and the Facebook Graph API

Why automate this

Posting a daily English-learning card on Facebook sounds tiny β€” pick a phrase, write a meaning in Burmese, add three example sentences, attach an illustration, publish. Multiply that by 365 and the only sustainable answer is to automate the entire pipeline. The objective for this project was exactly that: fully automate generation, persistence, and Facebook publishing of daily learning content, on a stack we already trust β€” Laravel Actions, structured AI agents, image generation, and the Facebook Graph API.

System architecture

The system is split into three deliberately thin layers so each one can be tested and replaced in isolation.

  • Application layer. GenerateDailyVocabulary is the orchestrator action. The DailyVocabulary Eloquent model stores the final record. FacebookService owns the publish call and nothing else.
  • AI layer. An EnglishTeacher agent returns structured fields (not free text). Image generation reuses a phrase-aligned prompt produced by the same agent, so the picture and the wording stay coherent. The output format is schema-driven from the start.
  • External layer. The Facebook Graph API endpoint /{page-id}/photos is the only outbound integration. It needs a valid Page token and the right permissions, and any error is surfaced through the Laravel log so we can diagnose without re-running the agent.

The data contract

The agent response is treated like an API payload, not a chat reply. The schema has five tagged fields: phrase, content_type, localized_meaning, example_sentences (exactly 3), and image_prompt.

content_type uses a strict allow-list: phrasal_verb, idiom, collocation, expression, noun, verb, adjective, adverb. That single field then drives:

  • the dynamic post title (e.g. "Phrasal Verb of the Day"),
  • the type-specific hashtag,
  • and the layout choices in the caption builder.

Forcing example_sentences to be exactly three keeps the post layout predictable, which means the caption template never has to branch on count. Schema constraints like these are the single biggest reason we can skip brittle parsing logic and runtime formatting fallbacks.

End-to-end flow

The action runs eight steps in a fixed order:

  • Trigger. The route /ai/daily-vocabulary invokes GenerateDailyVocabulary.
  • Generate. Request a structured payload from EnglishTeacher.
  • Deduplicate. Check whether the phrase already exists using a case-insensitive query.
  • Create image. Render a square image using the agent's image_prompt.
  • Store image. Save it to the public disk and capture the URL/path.
  • Persist. Insert phrase, content type, meaning, examples, prompts, and image URL into the database.
  • Build caption. Pick a style variant and format the sections.
  • Publish. Upload the image and caption to the Graph API.

Caption generation

The caption builder has two halves: a strict format spec and a small safety net.

Required behaviors

  • Header: {Type} of the Day: {Phrase}.
  • A Burmese meaning block with a labeled section.
  • An examples block with a blank line between each example, so it reads cleanly on Facebook.
  • Style variants rotate (emoji header, bullet examples, a CTA, a base hashtag set) so the feed never looks copy-pasted.
  • The type hashtag is mapped from content_type.

Fallback and safety

  • Any invalid content_type falls back to expression instead of throwing.
  • The mapping uses a strict allow-list, so we cannot accidentally publish a label like "Verb_phrase of the Day" if the agent drifts.
  • Every final post still includes the campaign hashtags plus the type hashtag.

A real generated post

This is a verbatim runtime sample of what gets pushed to the Page:


πŸ“š Phrasal Verb of the Day: Run Out Of

πŸ‡²πŸ‡² Meaning (α€‘α€“α€­α€•α€Ήα€•α€¬α€šα€Ί):
တစ်ခုခု α€€α€―α€”α€Ία€žα€½α€¬α€Έα€žα€Šα€Ί (α€žα€­α€―α€·) α€•α€Όα€α€Ία€œα€•α€Ία€žα€½α€¬α€Έα€žα€Šα€Ία‹

πŸ“ Examples (α€₯ပမာ စာကြောင်းများ):

πŸ”Ή We have run out of milk, so I need to go to the grocery store.

πŸ”Ή We need to make a decision soon because we are running out of time.

πŸ”Ή My phone just ran out of battery, so I could not call you.

✨ Save this post and try using it in your conversation today!

#LearnEnglishDaily #PhrasalVerbOfTheDay

Database and storage

The DailyVocabulary row keeps everything we need to either re-render the post later or audit what the agent produced:

  • phrase (unique)
  • content_type
  • localized_meaning
  • example_sentences (stored as JSON)
  • image_prompt
  • image_url

The image file itself lives under storage/app/public/vocabulary. The public URL is generated from the disk configuration, and the relative path is what we hand to the Facebook upload as the multipart source.

Facebook integration

The publish call is intentionally small.

  • Endpoint: https://graph.facebook.com/v25.0/{page-id}/photos
  • Multipart payload:
    • source β€” the image binary,
    • caption β€” the final formatted text,
    • access_token β€” the Page token.
  • Configuration is read from config/services.php, which in turn reads .env.
  • Success returns the Facebook response JSON and the action continues gracefully.
  • Failure logs both the HTTP status and the response body, so diagnosing a bad token or a missing permission does not require re-running the agent.

Permissions and Meta compliance

The Page token needs:

  • pages_show_list
  • pages_manage_posts

Avoid the deprecated publish_actions permission β€” it is no longer accepted. On the Meta App side, the app must also expose:

  • a public HTTPS Privacy Policy URL,
  • a Data Deletion Instructions or Callback URL,
  • and no localhost or private URLs in app settings.

These three are easy to forget on the first submission and are the most common reason the app gets rejected during review.

Demo runbook

The exact sequence used for a live demo:

  • 1) php artisan migrate
  • 2) php artisan serve
  • 3) Open: http://127.0.0.1:8000/ai/daily-vocabulary
  • 4) php artisan tinker
  • App\Models\DailyVocabulary::latest()->first(['phrase','content_type','image_url']);
  • 5) tail -f storage/logs/laravel.log

During the walkthrough it helps to point out three things in order: the generated card in the browser, the matching DB row with content_type and image_url, and the success/failure entry in the log for the Facebook step.

Lessons learned

  • Schema-first AI removes whole classes of bugs. Treating the agent response like an API payload (phrase, content_type, localized_meaning, example_sentences, image_prompt) means the caption builder never has to guess.
  • Allow-lists beat string matching. A strict content_type allow-list with an expression fallback keeps the post header sane even when the model drifts.
  • Log the bad path loudly. Logging the Graph API status and response body on failure turns "the cron broke" into a 30-second diagnosis instead of a re-run.

Page Top