Vue Laravel Production Tips
April 6, 2025 · AI Conversations
@section('plugins.Summernote', true)
@section('plugins.icheckBootstrap', true)
@section('plugins.TempusDominusBs4', true)
{{– Customize layout sections –}}
@section('subtitle', __('Create – Survey'))
{{– Content body: main page content –}}
@section('content_body')
{{ __('Create Surveys') }}
@if($errors->any())
@foreach ($errors->all() as $error)
{{ $error }}
@endforeach
@endif
@if(session()->has('message.level'))
{!! session('message.content') !!}
@endif
1. Survey
2. Pillers
3. Questions
Create Survey
Name
@{{ surveyError }}
{{ __('Allow comments on Survey') }}
{{ __('Allow comments on Pillers') }}
{{ __('Mark as External') }}
Save and Next
{{ __('Add Pillers') }}
{{ __('Piller') }} @{{ index + 1 }}
{{ __('Title') }} (@{{ lang.code.toUpperCase() }})
{{ __('Weight') }}
{{ __('Position') }}
{{ __('Add More Pillers') }}
{{ __('Save and Next') }}
Add Question
Piller
— Select Piller —
@{{ pill.title_en }}
Title (EN)
Title (DE)
Position
Submit
@stop
{{– Push extra scripts –}}
@push('js')
<script>
window.availableLanguages = @json(getLanguages());
console.log(window.availableLanguages);
</script>
<script>
import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
document.addEventListener('DOMContentLoaded', () => {
const app = createApp({
data() {
return {
message: 'Hello Vue!',
step: 1,
survey: {
name: '',
options: []
},
surveyError: '',
survey_id: null,
availableLanguages: window.availableLanguages || [],
pillers: [
{
titles: {}, // dynamically assigned
weight: '',
position: ''
}
],
question: {
piller_id: '',
title_en: '',
title_de: '',
position: ''
}
}
},
mounted() {
// Init first piller titles
this.availableLanguages.forEach(lang => {
this.pillers[0].titles[lang.code] = '';
});
},
methods: {
goToStep(step) {
if (step === 2 && !this.survey_id) return;
if (step === 3 && this.pillers.every(p => !p.id)) return;
this.step = step;
},
addPiller() {
if (this.pillers.length >= 5) return;
const newPiller = {
titles: {},
weight: '',
position: ''
};
this.availableLanguages.forEach(lang => {
newPiller.titles[lang.code] = '';
});
this.pillers.push(newPiller);
},
removePiller(index) {
this.pillers.splice(index, 1);
},
submitSurvey() {
this.surveyError = '';
if (!this.survey.name) {
this.surveyError = 'Survey name is required.';
return;
}
const data = {
name: this.survey.name,
allow_comments: this.survey.options.includes('allow_comments') ? 1 : 0,
allow_comments_on_pillers: this.survey.options.includes('allow_comments_on_pillers') ? 1 : 0,
mark_as_external: this.survey.options.includes('mark_as_external') ? 1 : 0,
};
fetch("{{ route('admin.surveys.store') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify(data)
})
.then(res => res.json().then(json => ({ status: res.status, body: json })))
.then(({ status, body }) => {
if (status === 200 && body.success) {
this.survey_id = body.survey_id;
Swal.fire({
icon: 'success',
title: "{{ __('Survey saved successfully!') }}",
showConfirmButton: false,
timer: 1500
});
this.step = 2;
} else {
Swal.fire({
icon: 'error',
title: "{{ __('Validation Error') }}",
text: body.message || "{{ __('Please check your input.') }}"
});
}
})
.catch(err => {
Swal.fire({
icon: 'error',
title: "{{ __('Unexpected Error') }}",
text: "{{ __('Something went wrong while saving the survey.') }}"
});
});
},
submitPillers() {
if (this.pillers.length !== 5) {
Swal.fire('Validation Error', 'You must add exactly 5 Pillers to continue.', 'warning');
return;
}
// Validate each piller
for (let [i, piller] of this.pillers.entries()) {
for (let lang of this.availableLanguages) {
if (!piller.titles[lang.code]) {
Swal.fire('Validation Error', `Piller ${i + 1}: Title required for ${lang.title}`, 'error');
return;
}
}
if (!piller.weight || !piller.position) {
Swal.fire('Validation Error', `Piller ${i + 1}: Weight and Position are required`, 'error');
return;
}
}
const data = {
survey_id: this.survey_id,
pillers: this.pillers
};
fetch("{{ route('admin.pillars.store') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify(data)
})
.then(res => res.json().then(json => ({ status: res.status, body: json })))
.then(({ status, body }) => {
if (status === 200 && body.success) {
// Save returned piller_ids if needed
this.pillers = body.pillers;
Swal.fire({
icon: 'success',
title: "{{ __('Pillers saved successfully!') }}",
showConfirmButton: false,
timer: 1500
});
this.step = 3;
} else {
Swal.fire({
icon: 'error',
title: "{{ __('Validation Error') }}",
text: body.message || "{{ __('Please check your input.') }}"
});
}
})
.catch(() => {
Swal.fire({
icon: 'error',
title: "{{ __('Unexpected Error') }}",
text: "{{ __('Something went wrong while saving the Pillers.') }}"
});
});
},
submitQuestion() {
this.message = 'Question added!';
this.question = { piller_id: '', title_en: '', title_de: '', position: '' };
}
}
});
app.mount('#surveyApp');
});
</script>
@endpush
for production
—
### ✅ **1. Remove Debug Statements**
Remove `console.log(window.availableLanguages);`.
// Remove this in production
console.log(window.availableLanguages);
—
### ✅ **2. Handle CSRF via Meta Tag (optional but cleaner)**
Instead of embedding the CSRF token directly in JS using `{{ csrf_token() }}`, use Laravel's meta tag and read it from JS:
**Blade (“ section):**
<meta name="csrf-token" content="{{ csrf_token() }}">
**In Vue method:**
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
Use it:
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
}
—
### ✅ **3. Handle Network Errors and Fallbacks**
Make error messages user-friendly and robust:
.catch(error => {
console.error('Request failed', error);
Swal.fire({
icon: 'error',
title: "Server Error",
text: "Unable to process your request. Please try again later."
});
});
—
### ✅ **4. Add Loader During Submission**
To improve UX, show a loader or disable buttons during async actions.
<button class="btn btn-primary" @click="submitSurvey" :disabled="loading">
<span v-if="loading" class="spinner-border spinner-border-sm"></span>
Save and Next
</button>
data() {
return {
loading: false,
...
}
},
methods: {
async submitSurvey() {
this.loading = true;
try {
...
} finally {
this.loading = false;
}
}
}
—
### ✅ **5. Enable Strict Type Handling (optional but helpful)**
If you have control over your backend and frontend schemas, validate and cast data properly in the Laravel controller (`request()->validate()` and type casts).
—
### ✅ **6. Defer Non-Essential Scripts**
For performance, make sure you're not loading large plugins unless required. Only include:
@section('plugins.Summernote', false) // set true only if needed
—
### ✅ **7. Optimize for Accessibility**
Label every field clearly and use `aria-*` where needed for accessibility compliance.
—
### ✅ **8. Localization Readiness**
You're already using `__()` – make sure all dynamic strings (like validation messages) are translated via Laravel's `lang` files.
—
### ✅ **9. Vuex or Pinia for Larger State**
If this multi-step wizard grows more complex, you can modularize state using Pinia (preferred for Vue 3).
—
### ✅ **10. Production Build and Asset Versioning**
Ensure `vite` or `mix` is building the production assets with versioning enabled:
yarn build
# or
npm run build
In Blade:
<script type="module" src="{{ mix('js/app.js') }}"></script>
—
Let me know if you’d like me to split Vue templates into `.vue` components or refactor logic into Pinia/Vuex for scaling the app!
<script type="module">
import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
…is **totally valid and fine for production**, especially if:
– It's a **small scope** Vue instance (like your 3-step survey wizard),
– You're not relying on a build system (like Vite/Yarn/Webpack),
– And you want to **keep things simple** and easily maintainable.
—
### ✅ To Summarize for Production Usage (CDN-based Vue)
Here’s what you **should** ensure:
#### 1. ✅ Use the CDN with version locking:
<script type="module">
import { createApp } from 'https://unpkg.com/vue@3.4.21/dist/vue.esm-browser.js';
🔐 This prevents future unexpected breakage from Vue updates.
—
#### 2. ✅ Minify if needed
Use `.prod` version if you want a slightly more optimized version:
import { createApp } from 'https://unpkg.com/vue@3.4.21/dist/vue.esm-browser.prod.js';
—
#### 3. ✅ Remove dev-only code
– Remove all `console.log()`
– Avoid dev alerts or placeholders like `message: 'Hello Vue!'`
—
#### 4. ✅ Handle missing script fallback (optional)
You can add a “ warning and optionally fallback logic if Vue fails to load — mostly for edge-case users.
—
#### 5. ✅ Cache Vue via CDN using HTML meta tags (optional but improves perf)
Set CDN caching headers using:
<link rel="preload" as="script" href="https://unpkg.com/vue@3.4.21/dist/vue.esm-browser.prod.js">
—
If you’re happy with this approach and the Vue section is isolated, there’s **no need for Yarn, npm, or Vite**.
Let me know if you want to optimize any part of the form (e.g., dynamically importing components, making it reusable elsewhere, or integrating Axios via CDN too).
@section('plugins.Summernote', true)
@section('plugins.icheckBootstrap', true)
@section('plugins.TempusDominusBs4', true)
{{– Customize layout sections –}}
@section('subtitle', __('Create – Survey'))
{{– Content body: main page content –}}
@section('content_body')
{{ __('Create Surveys') }}
@if($errors->any())
@foreach ($errors->all() as $error)
{{ $error }}
@endforeach
@endif
@if(session()->has('message.level'))
{!! session('message.content') !!}
@endif
1. Survey
2. Pillers
3. Questions
Create Survey
Name
@{{ surveyError }}
{{ __('Allow comments on Survey') }}
{{ __('Allow comments on Pillers') }}
{{ __('Mark as External') }}
Save and Next
{{ __('Add Pillers') }}
{{ __('Piller') }} @{{ index + 1 }}
{{ __('Title') }} (@{{ lang.code.toUpperCase() }})
{{ __('Weight') }}
{{ __('Position') }}
{{ __('Add More Pillers') }}
{{ __('Save and Next') }}
Add Question
Piller
— Select Piller —
@{{ pill.title_en }}
Title (EN)
Title (DE)
Position
Submit
@stop
{{– Push extra scripts –}}
@push('js')
<script>
window.availableLanguages = @json(getLanguages());
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
</script>
<script>
import { createApp } from 'https://unpkg.com/vue@3.5.13/dist/vue.esm-browser.prod.js'
document.addEventListener('DOMContentLoaded', () => {
const app = createApp({
data() {
return {
message: 'Hello Vue!',
loading: false,
step: 1,
survey: {
name: '',
options: []
},
surveyError: '',
survey_id: null,
availableLanguages: window.availableLanguages || [],
pillers: [
{
titles: {}, // dynamically assigned
weight: '',
position: ''
}
],
question: {
piller_id: '',
title_en: '',
title_de: '',
position: ''
}
}
},
mounted() {
// Init first piller titles
this.availableLanguages.forEach(lang => {
this.pillers[0].titles[lang.code] = '';
});
},
methods: {
goToStep(step) {
if (step === 2 && !this.survey_id) return;
if (step === 3 && this.pillers.every(p => !p.id)) return;
this.step = step;
},
addPiller() {
if (this.pillers.length >= 5) return;
const newPiller = {
titles: {},
weight: '',
position: ''
};
this.availableLanguages.forEach(lang => {
newPiller.titles[lang.code] = '';
});
this.pillers.push(newPiller);
},
removePiller(index) {
this.pillers.splice(index, 1);
},
async submitSurvey() {
this.surveyError = '';
if (!this.survey.name) {
this.surveyError = 'Survey name is required.';
return;
}
const data = {
name: this.survey.name,
allow_comments: this.survey.options.includes('allow_comments') ? 1 : 0,
allow_comments_on_pillers: this.survey.options.includes('allow_comments_on_pillers') ? 1 : 0,
mark_as_external: this.survey.options.includes('mark_as_external') ? 1 : 0,
};
this.loading = true;
try {
fetch("{{ route('admin.surveys.store') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": csrfToken
},
body: JSON.stringify(data)
})
.then(res => res.json().then(json => ({ status: res.status, body: json })))
.then(({ status, body }) => {
if (status === 200 && body.success) {
this.survey_id = body.survey_id;
Swal.fire({
icon: 'success',
title: "{{ __('Survey saved successfully!') }}",
showConfirmButton: false,
timer: 1500
});
this.step = 2;
} else {
Swal.fire({
icon: 'error',
title: "{{ __('Validation Error') }}",
text: body.message || "{{ __('Please check your input.') }}"
});
}
})
.catch(err => {
Swal.fire({
icon: 'error',
title: "{{ __('Unexpected Error') }}",
text: "{{ __('Something went wrong while saving the survey.') }}"
});
});
} catch (error) {
Swal.fire({
icon: 'error',
title: "{{ __('Unexpected Error') }}",
text: "{{ __('Something went wrong while saving the survey.') }}"
});
} finally {
this.loading = false;
}
},
submitPillers() {
if (this.pillers.length !== 5) {
Swal.fire('Validation Error', 'You must add exactly 5 Pillers to continue.', 'warning');
return;
}
// Validate each piller
for (let [i, piller] of this.pillers.entries()) {
for (let lang of this.availableLanguages) {
if (!piller.titles[lang.code]) {
Swal.fire('Validation Error', `Piller ${i + 1}: Title required for ${lang.title}`, 'error');
return;
}
}
if (!piller.weight || !piller.position) {
Swal.fire('Validation Error', `Piller ${i + 1}: Weight and Position are required`, 'error');
return;
}
}
const data = {
survey_id: this.survey_id,
pillers: this.pillers
};
this.loading = true;
try {
fetch("{{ route('admin.pillars.store') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": csrfToken
},
body: JSON.stringify(data)
})
.then(res => res.json().then(json => ({ status: res.status, body: json })))
.then(({ status, body }) => {
if (status === 200 && body.success) {
// Save returned piller_ids if needed
this.pillers = body.pillers;
Swal.fire({
icon: 'success',
title: "{{ __('Pillers saved successfully!') }}",
showConfirmButton: false,
timer: 1500
});
this.step = 3;
} else {
Swal.fire({
icon: 'error',
title: "{{ __('Validation Error') }}",
text: body.message || "{{ __('Please check your input.') }}"
});
}
})
.catch(() => {
Swal.fire({
icon: 'error',
title: "{{ __('Unexpected Error') }}",
text: "{{ __('Something went wrong while saving the Pillers.') }}"
});
});
} catch (error) {
Swal.fire({
icon: 'error',
title: "{{ __('Unexpected Error') }}",
text: "{{ __('Something went wrong while saving the Pillers.') }}"
});
} finally {
this.loading = false;
}
},
submitQuestion() {
this.message = 'Question added!';
this.question = { piller_id: '', title_en: '', title_de: '', position: '' };
}
}
});
app.mount('#surveyApp');
});
</script>
@endpush
some times it takes time to fully effect vue js on page
add loading till thrn
—
### ✅ Step 1: Add a Loading Overlay in Blade
Put this just **above** your `#surveyApp` div:
<div id="vue-loading" class="position-fixed w-100 h-100 d-flex justify-content-center align-items-center bg-white" style="z-index: 9999;">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
—
### ✅ Step 2: CSS (Optional for smooth fade-out)
Add this somewhere in your styles (or inline if needed):
<style>
#vue-loading.fade-out {
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
}
</style>
—
### ✅ Step 3: Hide Loader After Vue is Mounted
Inside your `DOMContentLoaded` callback (right **after** `app.mount('#surveyApp')`):
app.mount('#surveyApp');
// Hide loading spinner after mount
const loadingDiv = document.getElementById('vue-loading');
if (loadingDiv) {
loadingDiv.classList.add('fade-out');
setTimeout(() => loadingDiv.remove(), 500); // fully remove from DOM after fade
}
—
### ✅ Final Blade Snippet Reference
Just before `#surveyApp`:
<div id="vue-loading" class="position-fixed w-100 h-100 d-flex justify-content-center align-items-center bg-white" style="z-index: 9999;">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<div id="surveyApp" class="container-fluid mt-4">
<!-- Vue content -->
</div>
This ensures users see a clean loading state until Vue is fully reactive and ready. Let me know if you'd like to make the loader full-page or show a logo or message too.
—
### ✅ Use This Exact Loader Markup in Blade (Above `#surveyApp`)
<div id="vue-loading"
style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: white; z-index: 9999;"
class="d-flex justify-content-center align-items-center">
<div class="text-center">
<div class="spinner-border text-primary" role="status" style="width: 3rem; height: 3rem;">
<span class="sr-only">Loading...</span>
</div>
<div class="mt-2">Loading app...</div>
</div>
</div>
—
### ✅ Add This Optional CSS (for smooth fade-out):
<style>
#vue-loading.fade-out {
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
}
</style>
—
### ✅ Add Fade-Out Logic After Vue Mounts
Place this right after `app.mount('#surveyApp')` in your JS:
app.mount('#surveyApp');
const loadingDiv = document.getElementById('vue-loading');
if (loadingDiv) {
loadingDiv.classList.add('fade-out');
setTimeout(() => loadingDiv.remove(), 500); // Remove from DOM after fade
}
—
This will ensure:
– Full-screen white background loader
– Centered spinning loader and optional message
– Clean fade-out once Vue mounts
Let me know if you want to show a custom image/logo or animate the loading message.