Notify IndexNow after editorial publication with durable retry
This commit is contained in:
parent
d981a2aa4f
commit
5748124383
40
app/Console/Commands/RetryEditorialIndexNow.php
Normal file
40
app/Console/Commands/RetryEditorialIndexNow.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Editorial\PublicationEffects;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class RetryEditorialIndexNow extends Command
|
||||
{
|
||||
protected $signature = 'editorial:indexnow-retry';
|
||||
|
||||
protected $description = 'Retry due editorial IndexNow notifications without republishing content';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
if (! app()->environment(['production', 'testing']) || ! config('editorial.indexnow_enabled', app()->environment('production'))) {
|
||||
$this->line('IndexNow retry disabled in this environment.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
$ids = DB::table('editorial_publications')->where('effects->indexnow->status', 'pending')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('effects->indexnow->next_attempt_at')
|
||||
->orWhere('effects->indexnow->next_attempt_at', '<=', now()->timestamp);
|
||||
})->orderBy('updated_at')->limit(10)->pluck('id');
|
||||
$pending = 0;
|
||||
foreach ($ids as $id) {
|
||||
try {
|
||||
$receipt = app(PublicationEffects::class)->synchronize($id);
|
||||
$pending += ($receipt['effects']['indexnow']['status'] ?? '') === 'pending' ? 1 : 0;
|
||||
} catch (\Throwable) {
|
||||
$pending++;
|
||||
}
|
||||
}
|
||||
$this->line(json_encode(['processed' => count($ids), 'pending' => $pending]));
|
||||
|
||||
return $pending ? self::FAILURE : self::SUCCESS;
|
||||
}
|
||||
}
|
||||
97
app/Services/Editorial/IndexNowNotification.php
Normal file
97
app/Services/Editorial/IndexNowNotification.php
Normal file
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Editorial;
|
||||
|
||||
use App\Models\Articles;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/** Durable notification state lives in the publication receipt, alongside cache/search effects. */
|
||||
final class IndexNowNotification
|
||||
{
|
||||
public function initial(array $articles): array
|
||||
{
|
||||
if (! $this->enabled()) {
|
||||
return ['status' => 'skipped', 'reason' => 'disabled-environment'];
|
||||
}
|
||||
if (! collect($articles)->contains(fn ($a) => $a['content_changed'] ?? false)) {
|
||||
return ['status' => 'skipped', 'reason' => 'no-content-change'];
|
||||
}
|
||||
|
||||
return ['status' => 'pending', 'attempts' => 0];
|
||||
}
|
||||
|
||||
public function synchronize(array $state, array $published): array
|
||||
{
|
||||
if (($state['status'] ?? '') !== 'pending' || ! $this->enabled()
|
||||
|| ($state['next_attempt_at'] ?? 0) > now()->timestamp) {
|
||||
return $state;
|
||||
}
|
||||
|
||||
$base = rtrim((string) config('editorial.base_url'), '/');
|
||||
$host = parse_url($base, PHP_URL_HOST);
|
||||
// No per-article target, redirect, or credentials in URLs. Never probe public pages from local.
|
||||
if (! $host || ! preg_match('~\Ahttps://[a-z0-9.-]+\z~i', $base)
|
||||
|| $base !== rtrim((string) config('app.url'), '/')
|
||||
|| $host !== config('app.site_code')) {
|
||||
return $this->pending($state, 'site-binding-mismatch');
|
||||
}
|
||||
$key = config('key.indexnow');
|
||||
if (! is_string($key) || ! preg_match('/\A[a-zA-Z0-9-]{8,128}\z/', $key)) {
|
||||
return $this->pending($state, 'key-missing-or-invalid');
|
||||
}
|
||||
|
||||
$urls = [];
|
||||
foreach ($published as $item) {
|
||||
if (! ($item['content_changed'] ?? false)) {
|
||||
continue;
|
||||
}
|
||||
$article = Articles::with('oCategory')->find($item['article_id']);
|
||||
if (! $article || (int) $article->iStatus !== 1 || ! $article->oCategory
|
||||
|| (int) $article->oCategory->iStatus !== 1 || $article->created_at->isFuture()
|
||||
|| $article->bIsSeoExpired()) {
|
||||
continue;
|
||||
}
|
||||
$url = $base.'/a/'.$article->sSlug.'.html';
|
||||
if ($url === $item['url'] && preg_match('/\A[a-z0-9_-]+\z/', $article->sSlug)) {
|
||||
$urls[] = $url;
|
||||
}
|
||||
}
|
||||
$urls = array_values(array_unique($urls));
|
||||
if (! $urls) {
|
||||
return array_replace($state, ['status' => 'skipped', 'reason' => 'no-current-indexable-url']);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::connectTimeout(5)->timeout(15)->withoutRedirecting()->acceptJson()
|
||||
->post('https://api.indexnow.org/indexnow', [
|
||||
'host' => $host, 'key' => $key, 'keyLocation' => $base.'/'.$key.'.txt', 'urlList' => $urls,
|
||||
]);
|
||||
$code = $response->status();
|
||||
if (in_array($code, [200, 202], true)) {
|
||||
return ['status' => 'done', 'attempts' => ($state['attempts'] ?? 0) + 1,
|
||||
'http_status' => $code, 'urls' => $urls, 'submitted_at' => now()->toIso8601String(),
|
||||
'result' => $code === 202 ? 'received-key-validation-pending' : 'received'];
|
||||
}
|
||||
|
||||
return $this->pending($state, 'http-error', $code);
|
||||
} catch (\Throwable) {
|
||||
// Response bodies and exception messages may contain the key. Persist only safe diagnostics.
|
||||
return $this->pending($state, 'transport-error');
|
||||
}
|
||||
}
|
||||
|
||||
private function enabled(): bool
|
||||
{
|
||||
return app()->environment(['production', 'testing'])
|
||||
&& (bool) config('editorial.indexnow_enabled', app()->environment('production'));
|
||||
}
|
||||
|
||||
private function pending(array $state, string $reason, ?int $code = null): array
|
||||
{
|
||||
$attempts = ($state['attempts'] ?? 0) + 1;
|
||||
|
||||
return ['status' => 'pending', 'attempts' => $attempts, 'reason' => $reason, 'http_status' => $code,
|
||||
'last_attempt_at' => now()->toIso8601String(),
|
||||
'next_attempt_at' => now()->timestamp + min(86400, 300 * 2 ** min(9, $attempts - 1))];
|
||||
}
|
||||
}
|
||||
@ -36,6 +36,12 @@ public function synchronize(string $id): array
|
||||
}
|
||||
// Sitemap is generated from current DB rows, no external submission is performed.
|
||||
$effects['sitemap'] = ['status' => ($effects['cache']['status'] ?? '') === 'done' ? 'done' : 'pending', 'method' => 'database-driven'];
|
||||
// Legacy receipts were not queued for notification; never backfill them implicitly.
|
||||
$notification = $effects['indexnow'] ?? ['status' => 'skipped', 'reason' => 'legacy-publication'];
|
||||
if (($effects['cache']['status'] ?? '') === 'done') {
|
||||
$notification = app(IndexNowNotification::class)->synchronize($notification, $receipt['articles']);
|
||||
}
|
||||
$effects['indexnow'] = $notification;
|
||||
DB::table('editorial_publications')->where('id', $id)->update(['effects' => json_encode($effects), 'updated_at' => now()]);
|
||||
});
|
||||
|
||||
|
||||
@ -206,10 +206,13 @@ public function publish(string $id, array $input): array
|
||||
DB::table('editorial_documents')->where('content_key', $item['content_key'])->update(['article_id' => $articleId, 'published_revision' => $item['revision'], 'updated_at' => now()]);
|
||||
DB::table('editorial_versions')->insert(['content_key' => $item['content_key'], 'revision' => $item['revision'], 'snapshot' => json_encode($item, JSON_UNESCAPED_UNICODE), 'created_at' => now()]);
|
||||
$published[] = ['content_key' => $item['content_key'], 'article_id' => $articleId, 'revision' => $item['revision'],
|
||||
'content_changed' => $item['base_article_hash'] !== $this->articleHash(DB::table('articles')->where('id', $articleId)->first()),
|
||||
'url' => rtrim(config('editorial.base_url'), '/').'/a/'.$item['slug'].'.html'];
|
||||
}
|
||||
DB::table('editorial_publications')->insert(['id' => $data['request_id'], 'draft_id' => $id, 'payload_hash' => $opHash,
|
||||
'receipt' => json_encode(['publication_id' => $data['request_id'], 'status' => 'published', 'articles' => $published], JSON_UNESCAPED_UNICODE), 'created_at' => now(), 'updated_at' => now()]);
|
||||
'receipt' => json_encode(['publication_id' => $data['request_id'], 'status' => 'published', 'articles' => $published], JSON_UNESCAPED_UNICODE),
|
||||
'effects' => json_encode(['indexnow' => app(IndexNowNotification::class)->initial($published)]),
|
||||
'created_at' => now(), 'updated_at' => now()]);
|
||||
DB::table('editorial_drafts')->where('id', $id)->update(['status' => 'published', 'publication_id' => $data['request_id'], 'updated_at' => now()]);
|
||||
|
||||
return $data['request_id'];
|
||||
|
||||
@ -141,7 +141,8 @@ private function verify(string $file, array $state): array
|
||||
'body_sha256' => hash('sha256', $source['body_html']), 'like_growth' => $source['like_growth']];
|
||||
}
|
||||
$state['readback'] = $readback;
|
||||
$done = collect($state['receipt']['effects'])->every(fn ($e) => ($e['status'] ?? '') === 'done');
|
||||
$done = collect($state['receipt']['effects'])->every(fn ($e, $name) => ($e['status'] ?? '') === 'done'
|
||||
|| ($name === 'indexnow' && ($e['status'] ?? '') === 'skipped'));
|
||||
$state['phase'] = $done && collect($readback)->every(fn ($r) => $r['revision_matches'] && $r['content_matches']) ? 'complete' : 'published_pending';
|
||||
$this->save($file, $state);
|
||||
|
||||
|
||||
@ -7,4 +7,5 @@
|
||||
'preview_minutes' => 30,
|
||||
'max_batch' => 10,
|
||||
'search_index' => env('EDITORIAL_SEARCH_INDEX', env('APP_ENV') === 'local' ? 'fna_v3_articles' : null),
|
||||
'indexnow_enabled' => env('EDITORIAL_INDEXNOW_ENABLED', env('APP_ENV') === 'production'),
|
||||
];
|
||||
|
||||
@ -10,7 +10,7 @@ ## 职责与数据
|
||||
|
||||
项目正文源保留可编辑稿;`editorial_drafts`保存收到并渲染后的冻结版本,发布读取该快照,不临时重新读取文件。`articles`为公开显示数据,草稿不写入它。`editorial_documents`按稳定`content_key`绑定本站文章;`editorial_versions`保留已发布版本和首次接管旧文的基线版本0;`editorial_publications`保存独立发布回执及后续处理状态。图片元信息在`editorial_assets`。
|
||||
|
||||
新API不调用旧接收器,不发送旧Telegram通知、IndexNow提交或ATOM请求。旧资源接收接口继续沿用原流程,不在本次改造范围;普通编辑API拒绝免费节点与小火箭ID两个自动资源栏目。旧接口/人工直接编辑同一文章会造成草稿基线变化,新API发布时返回409,不能静默覆盖并行修改。
|
||||
新API不调用旧接收器,不发送旧Telegram通知或ATOM请求。2026-10-31起编辑文章新增/实质修改后具有独立IndexNow回执与失败重试,见[教程SEO通知](EDITORIAL_INDEXNOW.md);本地和测试不真实外发。旧资源接收接口继续沿用原流程,不在本次改造范围;普通编辑API拒绝免费节点与小火箭ID两个自动资源栏目。旧接口/人工直接编辑同一文章会造成草稿基线变化,新API发布时返回409,不能静默覆盖并行修改。
|
||||
|
||||
## 凭据与环境
|
||||
|
||||
@ -116,9 +116,9 @@ ### 3 发布确认过的版本
|
||||
|
||||
- `status: published`:本次内容已写入本站公开文章表;这是所调用环境的状态,本地成功不代表生产成功。
|
||||
- `articles`:各文章的`content_key`、`article_id`、`revision`和URL。
|
||||
- `effects.cache/search/sitemap`:各后续处理的独立结果。搜索暂时失败不撤销已提交正文,返回`pending`,不能重发正文来弥补搜索失败。
|
||||
- `effects.cache/search/sitemap/indexnow`:各后续处理的独立结果。搜索暂时失败不撤销已提交正文,返回`pending`,不能重发正文来弥补搜索失败。
|
||||
|
||||
后续处理为刷新本站响应缓存与热门数据缓存、同步本站搜索,以及使用数据库驱动的sitemap。缓存未刷新时sitemap也标记pending。不调用搜索引擎主动提交。
|
||||
后续处理为刷新本站响应缓存与热门数据缓存、同步本站搜索,以及使用数据库驱动的sitemap。缓存未刷新时sitemap也标记pending。IndexNow以独立`effects.indexnow`记录,不与sitemap结果混用,见[教程SEO通知](EDITORIAL_INDEXNOW.md)。
|
||||
|
||||
### 4 查询、重试与回退
|
||||
|
||||
|
||||
12
docs/maintenance/EDITORIAL_INDEXNOW.md
Normal file
12
docs/maintenance/EDITORIAL_INDEXNOW.md
Normal file
@ -0,0 +1,12 @@
|
||||
# 编辑文章 IndexNow
|
||||
|
||||
用户2026-10-31要求接入教程发布后的自动推送。新增或实际内容改变的编辑文章在发布事务写入IndexNow待处理状态,缓存刷新完成后提交当前有效URL。目标为IndexNow官方API,复用本站既有key及根目录验证文件,不生成/轮换密钥,不改旧资源接收器。
|
||||
|
||||
- `config/editorial.php` 的 `indexnow_enabled`:默认仅production开启,可用`EDITORIAL_INDEXNOW_ENABLED=false`关闭。local不会发出通知,测试使用Http fake。
|
||||
- `PublicationEffects`保存`effects.indexnow`,包括状态、次数、HTTP状态和下一次重试时间;不保存key、响应正文或异常消息。只有200/202记done;202表示收到并待验证key。提交被接收不等于搜索引擎已经抓取或收录,见[官方协议](https://www.indexnow.org/documentation)。
|
||||
- `editorial:indexnow-retry`挂接原Laravel调度,每5分钟检查一次,单次最多10个到期回执,防重叠;退避从5分钟递增,最大24小时。定时任务运行在FNA业务服务器,以原nginx身份执行,白色机仅SSH跳板。
|
||||
- 已done的通知不重复调用;同一发布UUID重放不重发文章。网络结果未知或提交后进程崩溃可能重复通知同一URL,按至少一次投递处理,不能承诺第三方只收到一次。每次重试都重新确认文章仍公开、栏目启用且可索引。
|
||||
- 内容未变、已撤下、不再允许索引时跳过;旧版本中没有IndexNow状态的历史发布回执不自动补推。暂时禁用不会丢弃已pending状态。
|
||||
- 仅推送同一站点的规范文章地址;app.url、editorial.base_url、app.site_code必须一致,不跟随重定向,不从文章输入指定外部推送目标。
|
||||
|
||||
状态:本地专项测试及回归已通过,正在准备生产部署;最终提交和服务器实测证据将在本次任务收尾更新。普通文章发布继续通过[白色机发布封装](EDITORIAL_RELEASE.md),公网编辑API保持关闭。
|
||||
@ -28,7 +28,7 @@ ## 调用
|
||||
- id.json仅`{"release_id":"原UUID"}`。服务器先保存稳定草稿ID/发布UUID,再调用API;相同bundle重复prepare不新建版本或重新抽取参数,相同release_id改正文拒绝409。改稿应显式创建新release并使用当前版本。
|
||||
- review.json的`data.previews`含原模板渲染的HTML,`data.review`含hash和digest。先阅读及检查冻结版本;程序渲染通过不等于人类/浏览器视觉验收。正式HTML内含线上资源链接,禁止直接在本机浏览器打开加载;视觉检查在本地对应稿件完成。必须线上页面验收时仍按项目访问批准规则到Windows,不以取预览为由越界。
|
||||
- publish.json为`{"release_id":"原UUID","expected_hash":"review中hash","review_digest":"review中digest"}`,不得盲填或以另一份稿件替换。程序先查回执;已发表则只重试后处理,草稿版本冲突返回失败。
|
||||
- status会从API重新查询并核对公开正文,不只读本地台账。`phase=complete`要求内容/版本/上传图哈希和缓存/搜索/sitemap状态通过;`published_pending`代表正文已提交但后续仍待处理,退出码2,不能据此另发正文。返回`published_at`来自服务器真实发布事务时间。
|
||||
- status会从API重新查询并核对公开正文,不只读本地台账。`phase=complete`要求内容/版本/上传图哈希和缓存/搜索/sitemap/IndexNow状态通过(无需通知的IndexNow skipped视为完成),且IndexNow通知为done或明确skipped;`published_pending`代表正文已提交但后续仍待处理,退出码2,不能据此另发正文。返回`published_at`来自服务器真实发布事务时间。
|
||||
|
||||
本机工具只输出结果文件路径及状态,不回显SSH认证、正文或预览。SSH断开、超时或非JSON输出都保存未知/失败状态,不自动重放写入;用原release_id查status。不存在草稿时沿原bundle恢复prepare。
|
||||
|
||||
@ -41,3 +41,7 @@ ## 状态与恢复
|
||||
上线前仅执行`2026_10_31_150000_add_article_display_timestamp.php`迁移(新增可空`sDisplayPublishedAt`);旧文显示回退原时间。新文草稿冻结0~21600秒偏移,首次发布计算展示时间;保留created_at、实际回执、点赞起点,旧文改稿不改展示日期。回滚程序保留新增可空列和历史发布记录,不删业务数据。
|
||||
|
||||
验证与当前部署状态见[2026-10-31发布记录](EDITORIAL_RELEASE_2026-10-31.md)。周期调度和额度唯一见[WEEKLY_TUTORIALS.md](WEEKLY_TUTORIALS.md)。
|
||||
|
||||
IndexNow的结果、环境开关及服务器延时重试见[教程SEO通知](EDITORIAL_INDEXNOW.md)。通知pending只重试后处理,不再次发表正文。
|
||||
|
||||
2026-10-31编辑发布已接入[IndexNow通知与重试](EDITORIAL_INDEXNOW.md),复用发布回执effects;通知pending不表示正文未发布,禁止为通知重发正文。
|
||||
|
||||
@ -19,6 +19,8 @@
|
||||
|
||||
Schedule::command('article:sync-likes')->everyFiveMinutes();
|
||||
|
||||
Schedule::command('editorial:indexnow-retry')->everyFiveMinutes()->environments(['production'])->withoutOverlapping(10);
|
||||
|
||||
|
||||
//Artisan::command('db:reCreate {sTableName?}', function ($sTableName = null) {
|
||||
// // 1. 没传参数?直接亮红牌!
|
||||
|
||||
@ -389,4 +389,92 @@ public function test_raw_script_markup_is_escaped_and_publish_request_id_cannot_
|
||||
$this->publish($next, $request)->assertConflict();
|
||||
$this->assertSame(1, DB::table('articles')->count());
|
||||
}
|
||||
private function enableEditorialIndexNow(): void
|
||||
{
|
||||
config(['editorial.indexnow_enabled' => true, 'editorial.base_url' => 'https://example.org',
|
||||
'app.url' => 'https://example.org', 'app.site_code' => 'example.org',
|
||||
'key.indexnow' => 'test-key-12345678']);
|
||||
// API authentication in these tests is bound to the same origin.
|
||||
file_put_contents($this->credential, json_encode(['project' => 'fna-v3', 'environment' => 'testing',
|
||||
'base_url' => 'https://example.org', 'schema' => 'gemdocs.credential.v1', 'fields' => ['api_key' => $this->token]]));
|
||||
}
|
||||
|
||||
public function test_editorial_indexnow_notifies_new_and_changed_content_once_but_not_noop(): void
|
||||
{
|
||||
$this->enableEditorialIndexNow();
|
||||
Http::fake(['api.indexnow.org/*' => Http::response('', 200)]);
|
||||
$item = $this->item();
|
||||
$item['category_slug'] = 'advanced';
|
||||
$draft = $this->draft([$item]);
|
||||
$id = (string) Str::uuid();
|
||||
$first = $this->publish($draft, $id)->assertOk()->assertJsonPath('effects.indexnow.status', 'done');
|
||||
$this->assertTrue($first->json('articles.0.content_changed'));
|
||||
$this->publish($draft, $id)->assertOk();
|
||||
$this->postJson('/api/editorial/v1/publications/'.$id.'/synchronize')->assertOk();
|
||||
Http::assertSentCount(1);
|
||||
Http::assertSent(fn ($r) => $r->url() === 'https://api.indexnow.org/indexnow'
|
||||
&& $r['host'] === 'example.org' && $r['urlList'] === ['https://example.org/a/example.html']);
|
||||
$noop = $this->draft([$this->item('example', 1)]);
|
||||
// The category change in this revision is itself meaningful.
|
||||
$this->publish($noop)->assertOk()->assertJsonPath('effects.indexnow.status', 'done');
|
||||
$same = $this->draft([$this->item('example', 2)]);
|
||||
$this->publish($same)->assertOk()->assertJsonPath('effects.indexnow.reason', 'no-content-change');
|
||||
Http::assertSentCount(2);
|
||||
$changed = $this->item('example', 3);
|
||||
$changed['body'] .= '<p>新的说明。</p>';
|
||||
$this->publish($this->draft([$changed]))->assertOk()->assertJsonPath('effects.indexnow.status', 'done');
|
||||
Http::assertSentCount(3);
|
||||
}
|
||||
|
||||
public function test_editorial_indexnow_failure_retries_with_backoff_without_republishing(): void
|
||||
{
|
||||
$this->enableEditorialIndexNow();
|
||||
Http::fake(['api.indexnow.org/*' => Http::sequence()->push('do not persist response secrets', 429)->push('', 202)]);
|
||||
$draft = $this->draft([$this->item()]);
|
||||
$result = $this->publish($draft)->assertOk()->assertJsonPath('status', 'published')
|
||||
->assertJsonPath('effects.indexnow.status', 'pending')->assertJsonPath('effects.indexnow.http_status', 429);
|
||||
$this->assertStringNotContainsString('do not persist', json_encode($result->json()));
|
||||
$this->artisan('editorial:indexnow-retry')->assertExitCode(0);
|
||||
Http::assertSentCount(1);
|
||||
$this->travel(6)->minutes();
|
||||
$this->artisan('editorial:indexnow-retry')->assertExitCode(0);
|
||||
$this->getJson('/api/editorial/v1/publications/'.$result->json('publication_id'))->assertOk()
|
||||
->assertJsonPath('effects.indexnow.status', 'done')->assertJsonPath('effects.indexnow.http_status', 202)
|
||||
->assertJsonPath('effects.indexnow.attempts', 2);
|
||||
$this->assertSame(1, DB::table('articles')->count());
|
||||
$this->assertSame(1, DB::table('editorial_versions')->count());
|
||||
$this->assertSame(1, DB::table('editorial_publications')->count());
|
||||
$this->artisan('editorial:indexnow-retry')->assertExitCode(0);
|
||||
Http::assertSentCount(2);
|
||||
}
|
||||
|
||||
public function test_editorial_indexnow_does_not_send_disabled_or_mismatched_hosts_or_legacy_receipts(): void
|
||||
{
|
||||
Http::fake();
|
||||
$draft = $this->draft([$this->item()]);
|
||||
$result = $this->publish($draft)->assertOk()->assertJsonPath('effects.indexnow.status', 'skipped');
|
||||
$this->enableEditorialIndexNow();
|
||||
$legacy = $result->json('publication_id');
|
||||
DB::table('editorial_publications')->where('id', $legacy)->update(['effects' => null]);
|
||||
$this->postJson('/api/editorial/v1/publications/'.$legacy.'/synchronize')->assertOk()
|
||||
->assertJsonPath('effects.indexnow.reason', 'legacy-publication');
|
||||
config(['app.site_code' => 'wrong.example']);
|
||||
$new = $this->draft([$this->item('another')]);
|
||||
$this->publish($new)->assertOk()->assertJsonPath('effects.indexnow.reason', 'site-binding-mismatch');
|
||||
Http::assertNothingSent();
|
||||
}
|
||||
|
||||
public function test_editorial_indexnow_retries_skip_withdrawn_or_expired_articles(): void
|
||||
{
|
||||
$this->enableEditorialIndexNow();
|
||||
Http::fake(['api.indexnow.org/*' => Http::response('', 503)]);
|
||||
$result = $this->publish($this->draft([$this->item()]))->assertOk();
|
||||
DB::table('articles')->update(['iStatus' => 0]);
|
||||
$this->travel(6)->minutes();
|
||||
$this->artisan('editorial:indexnow-retry')->assertExitCode(0);
|
||||
$this->getJson('/api/editorial/v1/publications/'.$result->json('publication_id'))->assertOk()
|
||||
->assertJsonPath('effects.indexnow.reason', 'no-current-indexable-url');
|
||||
Http::assertSentCount(1);
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user