Skip to content

Commit bc92abd

Browse files
committed
feat: SEO-friendly export filenames with global title, per-item file title, Vietnamese slugify
1 parent 34cfc10 commit bc92abd

5 files changed

Lines changed: 57 additions & 10 deletions

File tree

app/src/components/bulk-canvas-card.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ onMounted(() => {
8181
</div>
8282

8383
<!-- Label -->
84-
<div class="card__label">{{ item.title || 'Untitled' }}</div>
84+
<div class="card__label">{{ item.fileTitle || item.title || 'Untitled' }}</div>
8585
</div>
8686
</template>
8787

app/src/components/bulk-canvas-item-editor.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@ onUnmounted(() => {
7676
@update:modelValue="onParamUpdate('feature_image', $event)"
7777
/>
7878

79+
<!-- File title (for export filename, not rendered in template) -->
80+
<MpInput
81+
label="File Title"
82+
:modelValue="item.fileTitle"
83+
placeholder="Override export filename"
84+
@update:modelValue="onParamUpdate('fileTitle', $event)"
85+
/>
86+
7987
<!-- Title / Subtitle -->
8088
<MpInput
8189
label="Title"

app/src/components/bulk-canvas-toolbar.vue

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,20 @@ import { ref } from 'vue'
33
import MpButton from './ui/MpButton.vue'
44
import ImagePicker from './ImagePicker.vue'
55
6+
import MpInput from './ui/MpInput.vue'
7+
68
const props = defineProps<{
79
templates: Array<{ id: string; name: string; brand: string; layout: string; size: string }>
810
selectedTemplateId: string
11+
globalTitle: string
912
itemCount: number
1013
selectedCount: number
1114
rendering: boolean
1215
}>()
1316
1417
const emit = defineEmits<{
1518
'update:selectedTemplateId': [id: string]
19+
'update:globalTitle': [title: string]
1620
'add-files': [files: File[]]
1721
'add-url': [url: string]
1822
'toggle-select-all': []
@@ -63,6 +67,16 @@ function onImagePickerSelect(url: string) {
6367
</select>
6468
</div>
6569

70+
<!-- Global title for export filenames -->
71+
<div class="toolbar__section">
72+
<MpInput
73+
label="Export Name"
74+
:modelValue="globalTitle"
75+
placeholder="e.g. may-ep-nhua-servo"
76+
@update:modelValue="emit('update:globalTitle', $event)"
77+
/>
78+
</div>
79+
6680
<!-- Drop zone -->
6781
<div class="toolbar__section">
6882
<label class="toolbar__label">Add Images</label>

app/src/composables/use-bulk-canvas.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface BulkCanvasItem {
77
featureImage: string
88
title: string
99
subtitle: string
10+
fileTitle: string
1011
params: Record<string, string>
1112
previewHtml: string
1213
selected: boolean
@@ -25,11 +26,23 @@ interface TemplateInfo {
2526
let nextId = 1
2627
function uid() { return `bulk-${nextId++}` }
2728

29+
// Remove Vietnamese diacritics and slugify
30+
function slugify(text: string): string {
31+
return text
32+
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
33+
.replace(/[đĐ]/g, 'd')
34+
.toLowerCase()
35+
.replace(/[^a-z0-9]+/g, '-')
36+
.replace(/^-|-$/g, '')
37+
|| 'banner'
38+
}
39+
2840
export function useBulkCanvas() {
2941
const mediaStore = useMediaStore()
3042

3143
const templates = ref<TemplateInfo[]>([])
3244
const selectedTemplateId = ref('')
45+
const globalTitle = ref('')
3346
const items = ref<BulkCanvasItem[]>([])
3447
const editingItemId = ref<string | null>(null)
3548
const renderQueue = ref(0)
@@ -57,13 +70,20 @@ export function useBulkCanvas() {
5770
featureImage,
5871
title,
5972
subtitle: '',
73+
fileTitle: '',
6074
params: {},
6175
previewHtml: '',
6276
selected: true,
6377
loading: false,
6478
}
6579
}
6680

81+
// Generate export filename for an item at given index (1-based)
82+
function exportFilename(item: BulkCanvasItem, index: number): string {
83+
const base = slugify(item.fileTitle || globalTitle.value)
84+
return `${base}-${index}.png`
85+
}
86+
6787
async function addImages(files: File[]) {
6888
const uploads = files.map(async (file) => {
6989
const media = await mediaStore.uploadFile(file)
@@ -140,7 +160,8 @@ export function useBulkCanvas() {
140160
const item = items.value.find(i => i.id === id)
141161
if (!item) return
142162

143-
// Handle special keys
163+
// Handle special keys — auto-slugify fileTitle
164+
if (key === 'fileTitle') { item.fileTitle = slugify(value); return }
144165
if (key === 'title') item.title = value
145166
else if (key === 'subtitle') item.subtitle = value
146167
else if (key === 'feature_image') item.featureImage = value
@@ -182,11 +203,12 @@ export function useBulkCanvas() {
182203

183204
return {
184205
templates, selectedTemplateId, selectedTemplate,
206+
globalTitle,
185207
items, editingItemId, editingItem, renderQueue,
186208
loadTemplates,
187209
addImages, addImageUrls, removeItem,
188210
renderItem, renderAll,
189-
updateItemParam,
211+
updateItemParam, exportFilename,
190212
toggleItem, selectAll, deselectAll, toggleSelectAll, selectedItems,
191213
openEditor, closeEditor,
192214
}

app/src/views/BulkCreateView.vue

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ onMounted(() => canvas.loadTemplates())
1212
1313
// --- Download helpers (snapdom + jszip) ---
1414
15-
async function captureItem(id: string): Promise<{ name: string; blob: Blob } | null> {
15+
async function captureItem(id: string): Promise<{ id: string; blob: Blob } | null> {
1616
const card = document.querySelector(`[data-bulk-id="${id}"]`) as HTMLElement
1717
if (!card) return null
1818
@@ -46,9 +46,7 @@ async function captureItem(id: string): Promise<{ name: string; blob: Blob } | n
4646
card.style.overflow = origCardOverflow
4747
card.style.position = origCardPos
4848
49-
const item = canvas.items.value.find(i => i.id === id)
50-
const name = (item?.title || 'banner').replace(/[^a-zA-Z0-9\u00C0-\u024F\u1E00-\u1EFF ]/g, '').trim().substring(0, 50)
51-
return { name: `${name}-${id}.png`, blob }
49+
return { id, blob }
5250
}
5351
5452
async function downloadZip(ids: string[]) {
@@ -57,9 +55,12 @@ async function downloadZip(ids: string[]) {
5755
const JSZip = (await import('jszip')).default
5856
const zip = new JSZip()
5957
60-
for (const id of ids) {
61-
const result = await captureItem(id)
62-
if (result) zip.file(result.name, result.blob)
58+
for (let i = 0; i < ids.length; i++) {
59+
const result = await captureItem(ids[i]!)
60+
if (!result) continue
61+
const item = canvas.items.value.find(it => it.id === result.id)
62+
const filename = item ? canvas.exportFilename(item, i + 1) : `banner-${i + 1}.png`
63+
zip.file(filename, result.blob)
6364
}
6465
6566
const zipBlob = await zip.generateAsync({ type: 'blob' })
@@ -107,10 +108,12 @@ function onViewDrop(e: DragEvent) {
107108
<BulkCanvasToolbar
108109
:templates="canvas.templates.value"
109110
:selectedTemplateId="canvas.selectedTemplateId.value"
111+
:globalTitle="canvas.globalTitle.value"
110112
:itemCount="canvas.items.value.length"
111113
:selectedCount="canvas.selectedItems.value.length"
112114
:rendering="canvas.renderQueue.value > 0 || downloading"
113115
@update:selectedTemplateId="canvas.selectedTemplateId.value = $event"
116+
@update:globalTitle="canvas.globalTitle.value = $event"
114117
@add-files="canvas.addImages($event)"
115118
@add-url="canvas.addImageUrls([$event])"
116119
@toggle-select-all="canvas.toggleSelectAll()"

0 commit comments

Comments
 (0)