1(() => { 2 const $ = (s, r = document) => r.querySelector(s), $$ = (s, r = document) => [...r.querySelectorAll(s)]; 3 const uiLocale = document.documentElement.lang?.toLowerCase().startsWith('fr') ? 'fr' : 'en'; 4 const T = uiLocale === 'fr' ? { 5 findingBtn:'Rechercheâ¦', findingTitle:'Recherche de ton logementâ¦', findingCopy:'Lecture des informations publiques de lâannonce.', foundTitle:'Logement trouvé. Chargement des détailsâ¦', foundCopy:'Récupération du titre, des photos et des informations.', buildingTitle:'Création de lâaperçuâ¦', buildingCopy:'Encore quelques secondes.', createPreview:'Créer lâaperçu <span>â</span>', invalidUrl:'Ce lien ne ressemble pas à une annonce Airbnb. Il doit contenir /rooms/ suivi dâun numéro.', unableListing:'Impossible de récupérer cette annonce. Vérifie quâelle est publique, puis réessaie.', foundToast:'Annonce importée. Voici ton aperçu.', property:'Logement', listing:'Annonce Airbnb', photo:'photo', photos:'photos', onlyImages:'Seuls les fichiers image sont acceptés.', previewFail:'Lâaperçu nâa pas pu se mettre à jour. Clique sur Actualiser.', titleFirst:'Ajoute dâabord un titre.', photoFirst:'Ajoute au moins une photo.', openingPaypal:'Ouverture du paiement PayPalâ¦', checkoutFail:'Impossible de lancer le paiement.', generate:'Obtenir mon site <span>â</span>', buildFirst:'Crée dâabord un aperçu.', adminCopied:'Code admin copié.', adminCopyFail:'Copie le code manuellement.', languageTitle:'Ton annonce Airbnb est en français', languageCopy:'Dans quelle langue veux-tu créer ton site ?', languageNote:'Si tu choisis lâanglais, PageOptimus récupère la version anglaise publique de lâannonce.', frenchSite:'Créer le site en français', englishSite:'Create the site in English', domainConfirmed:'Domaine confirmé pour le SEO.', domainSuggestion:'Simple suggestion : confirme-le seulement sâil tâappartient.', regenerate:'Titre et description régénérés.' 6 } : { 7 findingBtn:'Finding propertyâ¦', findingTitle:'Finding your propertyâ¦', findingCopy:'Reading public listing details.', foundTitle:'Property found. Loading detailsâ¦', foundCopy:'Getting title, photos and listing information.', buildingTitle:'Building your previewâ¦', buildingCopy:'Almost there.', createPreview:'Create preview <span>â</span>', invalidUrl:'This link does not look like an Airbnb listing. It should contain /rooms/ followed by a number.', unableListing:'This listing could not be retrieved. Check that it is public, then try again.', foundToast:'Listing imported. Here is your preview.', property:'Property', listing:'Airbnb listing', photo:'photo', photos:'photos', onlyImages:'Only image files are supported.', previewFail:'The preview could not update. Click Refresh.', titleFirst:'Add a property title first.', photoFirst:'Add at least one property photo.', openingPaypal:'Opening PayPal checkoutâ¦', checkoutFail:'Checkout could not start.', generate:'Get my website <span>â</span>', buildFirst:'Build a preview first.', adminCopied:'Admin code copied.', adminCopyFail:'Copy the code manually.', languageTitle:'Your Airbnb listing is in French', languageCopy:'Which language do you want for your website?', languageNote:'If you choose English, PageOptimus retrieves the public English version of the listing.', frenchSite:'Créer le site en français', englishSite:'Create the site in English', domainConfirmed:'Domain confirmed for SEO.', domainSuggestion:'Suggestion only: confirm it only if you own it.', regenerate:'Title and description regenerated.' 8 }; 9 function emptyProject(){return {sourceUrl:'',bookingUrl:'',listingId:'',title:'',location:'',brandName:'',description:'',locale:'en',guests:'',bedrooms:'',beds:'',bathrooms:'',rating:'',reviewCount:'',amenities:[],reviews:[],latitude:'',longitude:'',siteDomain:'',siteDomainIsSuggestion:false,remoteImageUrls:[],price:'',priceText:'',currency:'',roomType:'',analyticsSiteId:'',adminPassword:'',metaTitle:'',metaDescription:'',template:'premium',primaryColor:'#153f36',accentColor:'#9d6f2d',contactPhone:'',contactEmail:'',whatsapp:''};} 10 const state={project:emptyProject(),localImages:[]}; 11 const els={lookupView:$('#lookupView'),workspaceView:$('#workspaceView'),listingUrl:$('#listingUrl'),fetchBtn:$('#fetchBtn'),lookupProgress:$('#lookupProgress'),progressTitle:$
11('#progressTitle'),progressCopy:$('#progressCopy'),lookupError:$('#lookupError'),backBtn:$('#backBtn'),chipImage:$('#chipImage'),chipTitle:$('#chipTitle'),chipLocation:$('#chipLocation'),photoCount:$('#photoCount'),photoStrip:$('#photoStrip'),extraPhotos:$('#extraPhotos'),refreshBtn:$('#refreshBtn'),checkoutBtn:$('#checkoutBtn'),previewFrame:$('#previewFrame'),previewAddress:$('#previewAddress'),openPreview:$('#openPreview'),toast:$('#toast'),adminPassword:$('#adminPassword'),copyAdminPassword:$('#copyAdminPassword'),languageModal:$('#languageModal'),languageTitle:$('#languageTitle'),languageCopy:$('#languageCopy'),languageNote:$('#languageNote'),chooseFrench:$('#chooseFrench'),chooseEnglish:$('#chooseEnglish'),seoScore:$('#seoScore'),seoScoreBar:$('#seoScoreBar'),seoChecklist:$('#seoChecklist'),serpTitle:$('#serpTitle'),serpUrl:$('#serpUrl'),serpDescription:$('#serpDescription'),confirmDomain:$('#confirmDomain'),regenerateSeo:$('#regenerateSeo'),domainStatus:$('#domainStatus')}; 12 let progressTimers=[]; 13 const clean=v=>v==null?'':String(v).trim(); 14 const roomId=url=>clean(url).match(/\/rooms\/(\d+)/i)?.[1]||'';
15 const validAirbnbUrl=url=>Boolean(roomId(url)); 16 function slug(v){return clean(v).normalize('NFD').replace(/[\u0300-\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,' ').trim().split(/\s+/).filter(w=>!['the','and','with','for','dans','avec','pour','une','les','des','villa','apartment','appartement','home','house','logement'].includes(w)).slice(0,2).join('').slice(0,24)||'mystay';} 17 function suggestDomain(title){return `${slug(title)}.com`;} 18 function autoMetaTitle(p){const city=clean(p.location).split(',')[0];return `${clean(p.title)}${city?` | ${city}`:''}`.slice(0,60);} 19 function autoMetaDescription(p){const d=clean(p.description).replace(/\s+/g,' ');return (d||(`${clean(p.title)} - ${clean(p.location)}`)).slice(0,158);} 20 function randomCode(length=12){const chars='ABCDEFGHJKLMNPQRSTUVWXYZ23456789',bytes=new Uint8Array(length);crypto.getRandomValues(bytes);return [...bytes].map(b=>chars[b%chars.length]).join('');} 21 function randomSiteId(){const raw=crypto.randomUUID?crypto.randomUUID().replace(/-/g,''):`${Date.now()}${Math.random().toString(36).slice(2)}`;return `po_${raw.slice(0,24)}`;} 22 function ensureDefaults(project){if(!project.analyticsSiteId)project.analyticsSiteId=randomSiteId();if(!project.adminPassword)project.adminPassword=randomCode(12);if(!project.template)project.template='premium';if(!project.primaryColor)project.primaryColor='#153f36';if(!project.accentColor)project.accentColor='#9d6f2d';if(!project.metaTitle)project.metaTitle=autoMetaTitle(project);if(!project.metaDescription)project.metaDescription=autoMetaDescription(project);if(!project.siteDomain){project.siteDomain=suggestDomain(project.title);project.siteDomainIsSuggestion=true;}return project;} 23 function toast(message,type=''){if(!els.toast)return;els.toast.textContent=message;els.toast.className=`toast ${type}`;els.toast.hidden=false;clearTimeout(toast.timer);toast.timer=setTimeout(()=>els.toast.hidden=true,3800);} 24 function showError(message){els.lookupError.textContent=message;els.lookupError.hidden=false;els.lookupProgress.hidden=true;} 25 function clearLookupState(){progressTimers.forEach(clearTimeout);progressTimers=[];els.lookupError.hidden=true;els.lookupProgress.hidden=true;} 26 function setScanning(){clearLookupState();els.lookupProgress.hidden=false;els.fetchBtn.disabled=true;els.fetchBtn.innerHTML=T.findingBtn;els.progressTitle.textContent=T.findingTitle;els.progressCopy.textContent=T.findingCopy;progressTimers.push(setTimeout(()=>{els.progressTitle.textContent=T.foundTitle;els.progressCopy.textContent=T.foundCopy},1800));progressTimers.push(setTimeout(()=>{els.progressTitle.textContent=T.buildingTitle;els.progressCopy.textContent=T.buildingCopy},4800));} 27 function stopScanning(){progressTimers.forEach(clearTimeout);progressTimers=[];els.lookupProgress.hidden=true;els.fetchBtn.disabled=false;els.fetchBtn.innerHTML=T.createPreview;} 28 function chooseSiteLanguage(){return new Promise(resolve=>{if(!els.languageModal)return resolve('fr');els.languageTitle.textContent=T.languageTitle;els.languageCopy.textContent=T.languageCopy;els.languageNote.textContent=T.languageNote;els.chooseFrench.textContent=T.frenchSite;els.chooseEnglish.textContent=T.englishSite;els.languageModal.hidden=false;document.body.classList.add('modal-open');const done=locale=>{els.languageModal.hidden=true;document.body.classList.remove('modal-open');els.chooseFrench.onclick=null;els.chooseEnglish.onclick=null;resolve(locale)};els.chooseFrench.onclick=()=>done('fr');els.chooseEnglish.onclick=()=>done('en')})} 29 async function fetchLocalizedListing(url,preferredLocale){try{const r=await fetch('/api/fetch-listing',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({url,preferredLocale})}),d=await r.json().catch(()=>({}));if(r.ok&&d.listing)return d.listing}catch{}return null} 30 async function acceptListing(listing,originalUrl='',options={}){let next=ensureDefaults({...emptyProject(),...listing,remoteImageUrls:Array.isArray(listing.remoteImageUrls)?listing.remoteImageUrls:[]});if(originalUrl&&!next.sourceUrl)next.sourceUrl=originalUrl;let chosen=next.locale==='fr'?'fr':'en';if(next.locale==='fr'&&!options.skipLanguagePrompt){chosen=await chooseSiteLanguage();if(chosen==='en'){const translated=await fetchLocalizedListing(originalUrl||next.sourceUrl,'en-US');if(translated)next=ensureDefaults({...emptyProject(),...translated,analyticsSiteId:next.analyticsSiteId,adminPassword:next.adminPassword,remoteImageUrls:Array.isArray(translated.remoteImageUrls)?translated.remoteImageUrls:[]});}}next.locale=chosen;if(chosen!==uiLocale){sessionStorage.setItem('pageoptimus.pendingListing',JSON.stringify({listing:next,url:originalUrl,skipLanguagePrompt:true}));location.href=chosen==='fr'?'/fr/':'/';return}state.project=next;state.localImages=[];
30stopScanning();toast(T.foundToast);await openWorkspace()} 31 async function fetchListing(){const url=els.listingUrl.value.trim();if(!validAirbnbUrl(url))return showError(T.invalidUrl);setScanning();try{const r=await fetch('/api/fetch-listing',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({url})}),d=await r.json().catch(()=>({}));if(!r.ok||!d.listing)throw Error(d.error||T.unableListing);await acceptListing(d.listing,url)}catch(e){stopScanning();showError(e.message||T.unableListing)}} 32 function populateEditor(){const p=state.project;$$('[data-key]').forEach(input=>{const key=input.dataset.key;if(key==='amenitiesText')input.value=(p.amenities||[]).join('\n');else input.value=p[key]??''});const image=p.remoteImageUrls?.[0]||'';els.chipImage.hidden=!image;if(image)els.chipImage.src=image;els.chipTitle.textContent=p.title||T.property;els.chipLocation.textContent=p.location||p.roomType||T.listing;if(els.adminPassword)els.adminPassword.value=p.adminPassword||'';renderPhotos();updateSeoUi();} 33 function syncEditor(){ $$('[data-key]').forEach(input=>{const key=input.dataset.key;if(key==='amenitiesText')state.project.amenities=input.value.split(/\n/).map(clean).filter(Boolean);else state.project[key]=input.value}); } 34 function photoEntries(){return [...(state.project.remoteImageUrls||[]).map((url,index)=>({type:'remote',url,index})),...state.localImages.map((item,index)=>({type:'local',item,index}))]} 35 function renderPhotos(){const entries=photoEntries();els.photoCount.textContent=`${entries.length} ${entries.length===1?T.photo:T.photos}`;els.photoStrip.innerHTML='';entries.forEach(entry=>{const box=document.createElement('div');box.className='photo-thumb';const img=document.createElement('img');if(entry.type==='remote')img.src=entry.url;else img.src=URL.createObjectURL(entry.item.blob);const remove=document.createElement('button');remove.type='button';remove.textContent='Ã';remove.addEventListener('click',()=>{if(entry.type==='remote')state.project.remoteImageUrls.splice(entry.index,1);else state.localImages.splice(entry.index,1);renderPhotos();updateSeoUi();renderPreview().catch(()=>{})});box.append(img,remove);els.photoStrip.append(box)});updateSeoUi()} 36 function blobToDataURL(blob){return new Promise((resolve,reject)=>{const reader=new FileReader();reader.onload=()=>resolve(reader.result);reader.onerror=reject;reader.readAsDataURL(blob)})} 37 async function resizeImage(file){if(!file.type.startsWith('image/'))throw Error(T.onlyImages);const bitmap=await createImageBitmap(file);const scale=Math.min(1,1800/Math.max(bitmap.width,bitmap.height)),canvas=document.createElement('canvas');canvas.width=Math.round(bitmap.width*scale);canvas.height=Math.round(bitmap.height*scale);canvas.getContext('2d').drawImage(bitmap,0,0,canvas.width,canvas.height);const blob=await new Promise(resolve=>canvas.toBlob(resolve,'image/jpeg',.82));return blob||file} 38 function computeSeo(){const p=state.project,photos=photoEntries().length;let score=0;const checks=[];const add=(ok,pts,label)=>{if(ok)score+=pts;checks.push({ok,label})};add(clean(p.title).length>=20&&clean(p.title).length<=75,15,uiLocale==='fr'?'Titre descriptif':'Descriptive title');add(clean(p.description).length>=280,15,uiLocale==='fr'?'Description détaillée':'Detailed description');add(Boolean(clean(p.location)),10,uiLocale==='fr'?'Localisation':'Location');add(photos>=5,15,uiLocale==='fr'?'5+ photos':'5+ photos');add((p.amenities||[]).length>=5,10,uiLocale==='fr'?'5+ équipements':'5+ amenities');add(Boolean(p.rating&&p.reviewCount),10,uiLocale==='fr'?'Note & avis':'Rating & reviews');add(clean(p.metaTitle).length>=28&&clean(p.metaTitle).length<=60,10,'Meta title');add(clean(p.metaDescription).length>=110&&clean(p.metaDescription).length<=160,10,'Meta description');add(Boolean(clean(p.siteDomain))&&!p.siteDomainIsSuggestion,3,uiLocale==='fr'?'Domaine confirmé':'Confirmed domain');add(Boolean(clean(p.contactPhone)||clean(p.contactEmail)||clean(p.whatsapp)),2,uiLocale==='fr'?'Contact direct':'Direct contact');return {score,checks}} 39 function updateSeoUi(){if(!els.seoScore)return;const {score,checks}=computeSeo();els.seoScore.textContent=`${score}/100`;els.seoScoreBar.style.width=`${score}%`;els.seoScoreBar.dataset.level=score>=80?'good':score>=55?'mid':'low';els.seoChecklist.innerHTML=checks.map(c=>`<span class="${c.ok?'ok':''}">
39${c.ok?'â':'â'} ${c.label}</span>`).join('');els.serpTitle.textContent=clean(state.project.metaTitle)||autoMetaTitle(state.project);els.serpUrl.textContent=state.project.siteDomainIsSuggestion?`https://${clean(state.project.siteDomain)} · suggestion`:(clean(state.project.siteDomain)?`https://${clean(state.project.siteDomain)}`:'https://your-domain.com');els.serpDescription.textContent=clean(state.project.metaDescription)||autoMetaDescription(state.project);if(els.domainStatus){els.domainStatus.textContent=state.project.siteDomainIsSuggestion?T.domainSuggestion:T.domainConfirmed;els.domainStatus.classList.toggle('confirmed',!state.project.siteDomainIsSuggestion&&Boolean(state.project.siteDomain));}} 40 async function renderPreview(){syncEditor();updateSeoUi();const local=await Promise.all(state.localImages.slice(0,8).map(x=>blobToDataURL(x.blob))),images=[...(state.project.remoteImageUrls||[]),...local].slice(0,9);const r=await fetch('/api/preview',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({project:state.project,images})});if(!r.ok)throw Error(T.previewFail);const html=await r.text();els.previewFrame.onload=()=>{try{els.previewFrame.contentWindow.scrollTo({top:0,left:0,behavior:'instant'})}catch{}};els.previewFrame.srcdoc=html;els.previewAddress.textContent=state.project.siteDomain||state.project.title||'Live website preview'} 41 async function openWorkspace(){populateEditor();window.scrollTo({top:0,left:0,behavior:'instant'});document.body.classList.add('workspace-active');els.lookupView.hidden=true;els.workspaceView.hidden=false;const sc=els.workspaceView.querySelector('.editor-scroll');if(sc)sc.scrollTop=0;try{await renderPreview()}catch(e){toast(e.message,'error')}} 42 function backToLookup(){document.body.classList.remove('workspace-active');els.workspaceView.hidden=true;els.lookupView.hidden=false;window.scrollTo({top:0,left:0,behavior:'instant'})} 43 function dbOpen(){return new Promise((resolve,reject)=>{const r=indexedDB.open('pageoptimus',1);r.onupgradeneeded=()=>r.result.createObjectStore('projects');r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error)})} 44 async function saveForCheckout(){const db=await dbOpen();return new Promise((resolve,reject)=>{const tx=db.transaction('projects','readwrite');tx.objectStore('projects').put({project:state.project,images:state.localImages,savedAt:Date.now()},'current');tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error)})} 45 async function checkout(){syncEditor();if(!state.project.title)return toast(T.titleFirst,'error');if(!(state.project.remoteImageUrls?.length||state.localImages.length))return toast(T.photoFirst,'error');els.checkoutBtn.disabled=true;els.checkoutBtn.textContent=T.openingPaypal;try{await saveForCheckout();location.href=`${uiLocale==='fr'?'/fr':''}/checkout/`}catch(e){toast(e.message,'error');els.checkoutBtn.disabled=false;els.checkoutBtn.innerHTML=T.generate}} 46 els.fetchBtn?.addEventListener('click',fetchListing);els.listingUrl?.addEventListener('keydown',e=>{if(e.key==='Enter')fetchListing()});els.listingUrl?.addEventListener('paste',()=>setTimeout(()=>{if(validAirbnbUrl(els.listingUrl.value))fetchListing()},80));els.backBtn?.addEventListener('click',backToLookup); 47 $$('[data-key]').forEach(input=>input.addEventListener('input',()=>{syncEditor();if(input.dataset.key==='siteDomain')state.project.siteDomainIsSuggestion=false;updateSeoUi();clearTimeout(renderPreview.timer);renderPreview.timer=setTimeout(()=>renderPreview().catch(()=>{}),420)})); 48 els.extraPhotos?.addEventListener('change',async e=>{try{for(const file of [...e.target.files].slice(0,Math.max(0,24-photoEntries().length)))state.localImages.push({name:file.name,blob:await resizeImage(file)});renderPhotos();await renderPreview();e.target.value=''}catch(err){toast(err.message,'error')}}); 49 els.refreshBtn?.addEventListener('click',()=>renderPreview().catch(e=>toast(e.message,'error')));els.openPreview?.addEventListener('click',()=>{const html=els.previewFrame.srcdoc;if(!html)return toast(T.buildFirst,'error');const blob=new Blob([html],{type:'text/html'}),url=URL.createObjectURL(blob);window.open(url,'_blank','noopener');setTimeout(()=>URL.revokeObjectURL(url),10000)});els.checkoutBtn?.addEventListener('click',checkout);els.copyAdminPassword?.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(state.project.adminPassword||'');toast(T.adminCopied)}catch{toast(T.adminCopyFail,'error')}}); 50 els.confirmDomain?.addEventListener('click',()=>{syncEditor();state.project.siteDomainIsSuggestion=false;updateSeoUi();renderPreview().catch(()=>{})});els.regenerateSeo?.addEventListener('click',()=>{syncEditor();state.project.metaTitle=autoMetaTitle(state.project);state.project.metaDescription=autoMetaDescription(state.project);const mt=$('[data-key="metaTitle"]'),md=$('[data-key="metaDescription"]');if(mt)mt.value=state.project.metaTitle;if(md)md.value=state.project.metaDescription;updateSeoUi();renderPreview().catch(()=>{});toast(T.regenerate)}); 51 (async()=>{const raw=sessionStorage.getItem('pageoptimus.pendingListing');
51if(!raw)return;sessionStorage.removeItem('pageoptimus.pendingListing');try{const pending=JSON.parse(raw);if(pending?.listing){if(els.listingUrl&&pending.url)els.listingUrl.value=pending.url;await acceptListing(pending.listing,pending.url||'',{skipLanguagePrompt:Boolean(pending.skipLanguagePrompt)})}}catch{}})(); 52})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.