$this->addField('webflow_refresh_on_publish', Checkbox::class, __('Recharger tous les contenus lors de la publication sur Webflow'), ['default' => true, 'tab' => __('Paramètres')]);
$this->addField('publish_on_save', Checkbox::class, __('Publier le site lors de la modification de contenus'), ['default' => true, 'tab' => __('Paramètres')]);
$this->addField('domains', Textarea::class, __('Domaines à télécharger'), ['translatable' => false, 'tab' => __('Paramètres')]);
+ $this->addField('exclude_domains', Textarea::class, __('Domaines à exclure'), ['translatable' => false, 'tab' => __('Paramètres')]);
$this->addField('locales_domains', Table::class, __('Langues'), ['translatable' => false, 'columns' => ['locale' => __('Code langue'), 'url' => __('URL')], 'tab' => __('Paramètres')]);
$this->addField('slack', Text::class, __('Notification slack'), ['translatable' => false, 'tab' => __('Paramètres')]);
$s = StaticSiteUploader::getSites();
$this->addField('texts', WebflowTexts::class, '', ['tab' => __('Textes'), 'translatable' => true, 'hint' => __('Modifier un texte ici ne produira aucun changement sur webflow')]);
$this->addField('images', WebflowImages::class, '', ['tab' => __('Images'), 'translatable' => true]);
$this->addField('seo', BunchOfFieldsMultiple::class, '', ['translatable' => true, 'edit_label' => '%url | %seo_title', 'allows_add' => false, 'allows_delete' => false, 'allows_clone' => false, 'allows_reorder' => false, 'bunch' => SEOPage::class, 'tab' => __('SEO')]);
+ $this->addField('former_sitemap', Code::class, __('Ancienne sitemap'), ['language' => 'xml', 'tab' => __('Redirections')]);
$this->addField('redirections', BunchOfFieldsMultiple::class, '', ['translatable' => false, 'edit_label' => '%from → %to', 'bunch' => Redirection::class, 'tab' => __('Redirections')]);
$this->addField('api', Hidden::class);
}
public function onSaving(): bool
{
$this->saveDataInWebflow();
+
+ $change = false;
+ $froms = [];
+ $r = $this->redirections;
+ foreach ($r as $redirection) {
+ $froms[] = trim($redirection['from']);
+ }
+
+ $sitemap = simplexml_load_string($this->former_sitemap);
+ $sitemap->registerXPathNamespace('s', "http://www.sitemaps.org/schemas/sitemap/0.9");
+ foreach ($sitemap->xpath('//s:loc') as $url) {
+ $u = parse_url($url);
+ $p = trim($u['path'], '/');
+ if (!in_array($p, $froms)) {
+ $change = true;
+ $r[] = ['from' => $p, 'to' => ''];
+ }
+ }
+ if ($change) {
+ $this->redirections = $r;
+ }
return parent::onSaving();
}
$page['og_description'] = $page['seo_description'];
}
- if (!$this->_compareArrays($api['seo'][$page['id']], $page)) {
+ if (isset($api['seo'][$page['id']]) && $api['seo'][$page['id']]['type'] !== 'page_html' && !$this->_compareArrays($api['seo'][$page['id']], $page)) {
$api['seo'][$page['id']] = $page;
$hasChanged = true;
Webflow::savePageMeta($page);
}
foreach ($images[$mainLocale] as $id => $alt) {
- $apiAlt = $api['images'][$id]['alt'] ?? str_starts_with($api['images'][$id]['alt'], '__wf_') ? '' : $api['images'][$id]['alt'];
+ $apiAlt = $api['images'][$id]['alt'];
+ if (str_starts_with($apiAlt, '__wf_')) {
+ $apiAlt = '';
+ }
+ if (null === $alt) {
+ $alt = '';
+ }
if ($apiAlt != $alt) {
+ start_measure('saveImageAlt ' . $apiAlt . '!=' . $alt);
$api['images'][$id]['alt'] = $alt;
Webflow::saveImageAlt($id, $alt);
$hasChanged = true;
+ stop_measure('saveImageAlt ' . $apiAlt . '!=' . $alt);
}
if ($alt) {
{
start_measure("Webflow refresh data from api");
$lock = $this->getLock();
+ //dd(Webflow::getAllData($this->webflow)['cms']);
try {
Webflow::clearCache();
- $this->api = Webflow::getEditableData($this->webflow);
+ $this->api = $this->getEditableData();
$mainLocale = $this->getMainLocale();
$locales = $this->getLocalesCodes();
'uploads-ssl.webflow.com',
'uploads.webflow.com',
'cdn.prod.website-files.com',
- 'cdn.embedly.com',
] + \Cubist\Util\Text::splitLines($this->domains));
+ $exclude = array_unique([
+ ] + \Cubist\Util\Text::splitLines($this->exclude_domains));
+
$wget->setArg("domains", implode(',', $domains));
+ $wget->setArg("exclude-domains", implode(',', $exclude));
$wget->setArg("compression", 'auto');
if (!$force) {
$wget->setArg('N');
public function listPages()
{
- $seo = Webflow::getEditableData($this->webflow)['seo'];
+ $seo = $this->getEditableData()['seo'];
$res = [];
foreach ($seo as $s) {
$res[$s['id']] = $s['url'];
protected function compileLocale($locale)
{
$mirror = $this->getMirrorPath();
- $path = Files::mkdir(protected_path('webflow/final/' . $this->id . '/' . $locale));
+ $path = Files::emptyDir(protected_path('webflow/final/' . $this->id . '/' . $locale));
$rsync = new CommandLine\Rsync($mirror, $path, true);
$rsync->execute();
file_put_contents(Files::mkdir($path . '/css') . 'custom.css', $this->getCustomCSS());
file_put_contents(Files::mkdir($path . '/js') . 'custom.js', $this->getCustomJS());
+ $isMainLocale = $this->getMainLocale() === $locale;
+ $texts = $this->getTextTranslationsForCompilation($locale);
+ $images = $this->getTranslation('images', $locale);
+ $seo = $this->getTranslation('seo', $locale);
+
foreach (Files::getRecursiveDirectoryIterator($path) as $f) {
/** @var $f \SplFileInfo */
if ($f->isDir() || $f->getExtension() !== 'html') {
continue;
}
- $this->compileHTMLFile($f, $locale);
+ $this->compileHTMLFile($f, $isMainLocale, $locale, $texts, $images, $seo);
}
}
* @param $locale string
* @return void
*/
- protected function compileHTMLFile($f, $locale)
+ protected function compileHTMLFile($f, $isMainLocale, $locale, $texts, $images, $seo)
{
+
$html = file_get_contents($f->getPathname());
+ $regex = '/("https:\/\/' . $this->webflow . '.webflow.io\/\\\\")(.*)(\\\\"")/';
+ $html = preg_replace($regex, '\"$2\"', $html);
+
+ if (!preg_match('/data-wf-page="([^\"]+)"/', $html, $m)) {
+ return;
+ }
+ $pageId = $m[1];
+
+
$html = str_replace('</head>', '<link href="/css/custom.css" rel="stylesheet">' . "\n" . '</head>', $html);
$html = str_replace('</body>', '<script src="/js/custom.js"></script>' . "\n" . '</body>', $html);
$html = preg_replace('/lang=\"[a-zA-Z\-_]{2,6}\"/', 'lang="' . $locale . '"', $html);
+
// Texts
- $texts = $this->getTextTranslationsForCompilation($locale);
foreach ($texts as $text => $translation) {
- $html = str_replace($text, $translation, $html);
+ $html = str_replace('>' . $text . '<', '>' . $translation . '<', $html);
}
-
// Images
- $images = $this->getTranslation('images', $locale);
+
// SEO
- $html = preg_replace('/<title>[^<]*<\/title>/', '<title>Nimp</title>', $html);
+ if (!$isMainLocale) {
+ foreach ($seo as $s) {
+ if ($s['id'] === $pageId) {
+ $currentPage = $s;
+ break;
+ }
+ }
+
+ if (isset($currentPage)) {
+ $og_title = e($currentPage['og_title_copied'] ? $currentPage['seo_title'] : $currentPage['og_title']);
+ $og_desc = e($currentPage['og_description_copied'] ? $currentPage['seo_description'] : $currentPage['og_description']);
+
+ $html = preg_replace('/\s*<meta(.*)(name="description"|property="(og|twitter):\w+")(.*)\/>\s*/', '', $html);
+ $meta = '
+ <meta content="' . e($currentPage['seo_description']) . '" name="description" />
+ <meta content="' . $og_title . '" property="og:title" />
+ <meta content="' . $og_desc . '" property="og:description" />
+ <meta content="' . $og_title . '" property="twitter:title" />
+ <meta content="' . $og_desc . '" property="twitter:description" />';
+ $html = preg_replace('/<title>[^<]*<\/title>/', '<title>' . e($currentPage['seo_title']) . '</title>' . $meta, $html);
+ }
+ }
file_put_contents($f->getPathname(), $html);
}
Cache::put('webflow_' . $this->id . '_locales', $locales);
+
} catch (LockTimeoutException $e) {
} finally {
$lock?->forceRelease();
}
+ $seoTranslations = $this->getTranslations('seo');
+ foreach ($seoTranslations as $locale => $seo) {
+ $translation = [];
+ foreach ($seo as $item) {
+ if (!is_array($item) || !isset($item['id']) || !$item['id']) {
+ continue;
+ }
+ if (!isset($pages[$item['id']])) {
+ continue;
+ }
+ $translation[] = $item;
+ }
+
+
+ uasort($translation, function ($a, $b) {
+ if ($a['url'] === '/index.html') {
+ return -1000;
+ }
+ if ($b['url'] === '/index.html') {
+ return 1000;
+ }
+ return strcmp($a['url'], $b['url']);
+ });
+
+ $this->setTranslation('seo', $locale, $translation);
+ }
+
return parent::onRetrieved();
}
public function getEditableData()
{
Webflow::setToken($this->webflow_api_token);
- return Webflow::getEditableData($this->webflow);
+ return Webflow::getEditableData($this->webflow, $this->getMirrorPath());
}
public function getAvailableLocales()
namespace App\Services;
use App\Services\Webflow\Excel;
+use Cubist\Util\Files\Files;
use Cubist\Util\Html;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Cache;
public static function request($url, $data = [], $method = 'get', $ttl = 86400, $force = false)
{
- start_measure('Webflow API : ' . $url);
+ start_measure('Webflow API (' . $method . '): ' . $url);
$cacheKey = self::getCacheKey($url, $data, $method);
if ($force) {
Cache::forget($cacheKey);
}
/** @var Response $response */
$response = Http::withToken(self::getToken())->$method(self::BASE_URL . $url, $data);
- if ((int)$response->header('X-RateLimit-Remaining') <= 1) {
+ $xRateLimitRemaining = $response->header('X-RateLimit-Remaining') === '' ? 10 : (int)$response->header('X-RateLimit-Remaining');
+
+ if ($xRateLimitRemaining <= 1) {
+ start_measure('wait for API : ' . $response->header('X-RateLimit-Remaining'));
Cache::set('webflow_' . static::getToken() . '_wait', true, 60);
sleep(60);
+ stop_measure('wait for API : ' . $response->header('X-RateLimit-Remaining'));
}
return $response->json();
});
if (null === $res && !$force) {
return self::request($url, $data, $method, $ttl, true);
}
- stop_measure('Webflow API : ' . $url);
+ stop_measure('Webflow API (' . $method . '): ' . $url);
return $res;
}
];
}
+ //file_put_contents(Files::mkdir(protected_path('webflow/' . $shortname . '/')) . 'cms.json', json_encode($res));
+
return $res;
}
public static function listCMSCollections($shortname)
{
- return self::paginatedRequest('sites/' . self::getSiteId($shortname) . '/collections', 'collections');
+ return self::request('sites/' . self::getSiteId($shortname) . '/collections')['collections'];
}
public static function getPageContents($pageID)
return self::request('assets/' . $id, ['altText' => $alt], 'post', 0, true);
}
- public static function getEditableData($shortname)
+ public static function getEditableData($shortname, $mirrorPath)
{
start_measure('Webflow : get editable data');
$type = isset($details['publishedPath']) ? 'folder' : 'page';
if ($type === 'page') {
$url .= '.html';
+ if (!file_exists($mirrorPath . $url)) {
+ continue;
+ }
}
+
$url = '/' . $url;
}
}
- uasort($res['seo'], function ($a, $b) {
- if ($a['url'] === '/index.html') {
- return -1000;
+ $ignoreNames = ['slug', 'json'];
+ $allowTypes = ['RichText', 'PlainText'];
+
+ foreach ($data['cms'] as $collection) {
+ $fields = [];
+
+ foreach ($collection['details']['fields'] as $field) {
+ if (!in_array($field['slug'], $ignoreNames) && in_array($field['type'], $allowTypes)) {
+ $fields[$field['slug']] = $field['type'];
+ }
}
- if ($b['url'] === '/index.html') {
- return 1000;
+
+ foreach ($collection['contents'] as $item) {
+ foreach ($fields as $f => $type) {
+ if (!isset($item['fieldData'][$f])) {
+ continue;
+ }
+ $t = $item['fieldData'][$f];
+ if (!$t) {
+ continue;
+ }
+ if ($type === 'PlainText') {
+ $texts = [$t];
+ } else if ($type === 'RichText') {
+ $texts = Html::getTextNodes($t);
+ }
+ foreach ($texts as $t) {
+ if (!isset($res['texts'][$t])) {
+ $res['texts'][$t] = ['key' => base64_encode($t), 'occurences' => 0];
+ }
+ $res['texts'][$t]['occurences']++;
+ }
+ }
}
- return strcmp($a['url'], $b['url']);
- });
+ }
+
+ $res['seo'] = static::getPagesFromHTMLFiles($res['seo'], $mirrorPath);
+
+
self::$_editableData[$key] = $res;
stop_measure('Webflow : get editable data');
return self::$_editableData[$key];
}
+
+ public static function getPagesFromHTMLFiles($seo, $mirrorPath)
+ {
+ foreach (Files::getRecursiveDirectoryIterator($mirrorPath) as $f) {
+ /** @var $f \SplFileInfo */
+ if ($f->isDir() || $f->getExtension() !== 'html') {
+ continue;
+ }
+ $relative = '/' . ltrim(str_replace($mirrorPath, '', $f->getPathname()), "/");
+
+ $found = false;
+ foreach ($seo as $s) {
+ if ($s['url'] === $relative) {
+ $found = true;
+ break;
+ }
+ }
+ if ($found) {
+ continue;
+ }
+
+ $slug = $f->getBasename('.html');
+ $html = file_get_contents($f->getPathname());
+ $title = '';
+ $description = '';
+ if (preg_match('/<title>([^>]*)<\/title>/', $html, $matches)) {
+ $title = $matches[1];
+ }
+
+
+ $seo[$relative] = [
+ 'id' => $relative,
+ 'url' => $relative,
+ 'type' => 'page_html',
+ 'slug' => $slug,
+ 'draft' => false,
+ 'seo_title' => $title,
+ 'seo_description' => $description,
+ 'og_title' => $title,
+ 'og_description' => $description,
+ 'og_title_copied' => true,
+ 'og_description_copied' => true,
+ ];;
+ }
+
+ return $seo;
+ }
}