| Data de elaboração | 22/08/2025 |
| Responsável | Henrique Gomes Teixeira (Analista) |
| Equipe | Emanuel Lima (Analista), José Lucas (Analista) |
| Alvo | Sistemas que irão consumir notícias do Portal do Governo (WordPress) |
| Origem | Disseminar notícias oficiais, com consumo padronizado por aplicações externas. |
| Objetivo | Ensinar como integrar e apresentar notícias do WordPress em outros sistemas, garantindo performance, segurança, acessibilidade e governança de conteúdo. |
| Documentação correlata | API WP REST pública: https://rondonia.ro.gov.br/wp-json/wp/v2/posts |
| Observações | Recomenda-se interpor um Adapter/API de BFF para padronizar o payload e evitar acoplamento direto ao WP. |
per_page: quantas notícias (ex.: 4, 10).page: qual página (1 = mais recentes)._fields: quais campos queremos receber (para ficar leve).Importante: A API do WordPress do Portal retorna no máximo 10 notícias publicadas. Mesmo que você use
per_page=20ou tente avançar para páginas além da 1, o retorno não passa desse limite
Exemplos práticos:
https://rondonia.ro.gov.br/wp-json/wp/v2/posts?per_page=4&page=1&_fields=title,excerpt,link,yoast_head_json.og_imagehttps://rondonia.ro.gov.br/wp-json/wp/v2/posts?per_page=10&page=1&_fields=title,excerpt,link,yoast_head_json.og_imagetitle.renderedexcerpt.rendered (vem com <p>...</p>)linkyoast_head_json.og_image[0].urlDica: se quiser deixar mais rápido, sempre use
_fieldspara pedir só esses campos.
Abaixo, primeiro mostramos uma integração direta (rápida) e, depois, a integração recomendada com Adapter/BFF (mais robusta: cache, resiliente, padronizada).
public sealed class TitleDto { public string Rendered { get; set; } }
public sealed class ExcerptDto { public string Rendered { get; set; } }
public sealed class OgImageDto { public string Url { get; set; } }
public sealed class YoastHeadJsonDto
{
public List<OgImageDto> OgImage { get; set; }
}
public sealed class NoticiasDto
{
public TitleDto Title { get; set; }
public ExcerptDto Excerpt { get; set; }
public string Link { get; set; }
public YoastHeadJsonDto YoastHeadJson { get; set; }
}
using Refit;
public interface IPortalRondoniaWordpressClient
{
[Get("/wp-json/wp/v2/posts")]
[Headers(
"User-Agent: Portal-Integrador/1.0",
"Content-type: application/json; charset=utf-8")]
Task<List<NoticiasDto>> ObterUltimasNoticias(
[AliasAs("per_page")] int perPage = 4,
[AliasAs("page")] int page = 1,
[AliasAs("_fields")] string fields = "title,excerpt,link,yoast_head_json.og_image"
);
}
Instalação:
dotnet add package RefiteRefit.HttpClientFactory.
public sealed class NoticiaViewModel
{
public string Titulo { get; set; }
public string ResumoHtml { get; set; }
public string Link { get; set; }
public string UrlImagem { get; set; }
}
public static class NoticiaFactory
{
public static List<NoticiaViewModel> Criar(List<NoticiasDto> noticias)
{
return (noticias ?? new()).Select(c => new NoticiaViewModel
{
Titulo = System.Web.HttpUtility.HtmlDecode(c?.Title?.Rendered ?? string.Empty),
ResumoHtml = c?.Excerpt?.Rendered ?? string.Empty, // contém <p>...</p>
Link = c?.Link ?? "#",
UrlImagem = c?.YoastHeadJson?.OgImage?.FirstOrDefault()?.Url
}).ToList();
}
}
using Microsoft.Extensions.Caching.Memory;
public class HomeController : Controller
{
private readonly IPortalRondoniaWordpressClient _wp;
private readonly IMemoryCache _cache;
private readonly ILogger<HomeController> _logger;
public HomeController(IPortalRondoniaWordpressClient wp, IMemoryCache cache, ILogger<HomeController> logger)
{
_wp = wp; _cache = cache; _logger = logger;
}
[HttpGet]
public async Task<IActionResult> Noticias()
{
try
{
const string key = "ultimas-noticias:perPage=4:page=1";
var noticias = await _cache.GetOrCreateAsync(key, async e =>
{
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
return await _wp.ObterUltimasNoticias(perPage: 4, page: 1);
});
var vm = NoticiaFactory.Criar(noticias);
return PartialView("_Noticias", vm);
}
catch (ApiException ex)
{
_logger.LogError(ex, "Erro ao buscar Notícias");
// Fallback: devolve vazio (UI mostra placeholders)
return PartialView("_Noticias", new List<NoticiaViewModel>());
}
}
}
Views/Home/_Noticias.cshtml) — acessível e leve@model IEnumerable<NoticiaViewModel>
<div class="swiper swiper-container offer-slide" aria-label="Lista de notícias">
<div class="swiper-wrapper mb-4">
@foreach (var n in Model)
{
<div class="swiper-slide mb-3 mx-1" style="width:280px;">
<a href="@n.Link" target="_blank" rel="noopener noreferrer">
<div class="card border-0 text-white">
<img loading="lazy" style="object-fit:cover;height:170px"
src="@n.UrlImagem"
alt="Notícia: @n.Titulo" />
<div class="card-img-overlay custon-card-img-overlay">
<h5 class="card-title custon-card-title">@n.Titulo</h5>
</div>
</div>
</a>
</div>
}
</div>
<div class="swiper-button-next" aria-label="Próximo"></div>
<div class="swiper-button-prev" aria-label="Anterior"></div>
<div class="swiper-pagination" aria-label="Paginação"></div>
</div>
Dicas rápidas
• Useloading="lazy"em imagens.
• Mantenharel="noopener noreferrer"em links externos.
• SeUrlImagemfornull, exiba um placeholder local.
Por que um Adapter/BFF?
{
"items": [
{
"titulo": "string",
"resumoHtml": "string",
"link": "string",
"imagem": "string|null",
"fonte": "Portal do Governo"
}
],
"meta": { "page": 1, "perPage": 4, "total": null }
}
using Microsoft.Extensions.Caching.Memory;
using Polly;
using System.Net.Http.Json;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMemoryCache();
builder.Services.AddHttpClient("wp")
.AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(8)))
.AddPolicyHandler(Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.OrResult(r => !r.IsSuccessStatusCode)
.WaitAndRetryAsync(3, i => TimeSpan.FromMilliseconds(200 * i)));
// (Opcional) Rate limiting e CORS:
builder.Services.AddCors(o => o.AddDefaultPolicy(p => p.WithOrigins("https://seu-dominio.gov.br").AllowAnyHeader().AllowAnyMethod()));
// builder.Services.AddRateLimiter(...);
var app = builder.Build();
app.UseCors();
app.MapGet("/api/noticias", async (int page, int perPage, IMemoryCache cache, IHttpClientFactory http) =>
{
page = page <= 0 ? 1 : page;
perPage = perPage is <= 0 or > 20 ? 4 : perPage;
var key = $"wp:posts:{page}:{perPage}";
if (!cache.TryGetValue(key, out object data))
{
var client = http.CreateClient("wp");
var baseUrl = Environment.GetEnvironmentVariable("WP_BASE_URL")?.TrimEnd('/')
?? "https://rondonia.ro.gov.br";
var url = $"{baseUrl}/wp-json/wp/v2/posts?per_page={perPage}&page={page}&_fields=title,excerpt,link,yoast_head_json.og_image";
var resp = await client.GetAsync(url);
resp.EnsureSuccessStatusCode();
var json = await resp.Content.ReadFromJsonAsync<List<NoticiasDto>>();
var items = (json ?? new()).Select(c => new {
titulo = System.Web.HttpUtility.HtmlDecode(c?.Title?.Rendered ?? ""),
resumoHtml = c?.Excerpt?.Rendered,
link = c?.Link,
imagem = c?.YoastHeadJson?.OgImage?.FirstOrDefault()?.Url,
fonte = "Portal do Governo"
});
data = new { items, meta = new { page, perPage, total = (int?)null } };
cache.Set(key, data, TimeSpan.FromMinutes(5));
}
return Results.Json(data);
});
app.Run();
Consumo pelos sistemas:
GET /api/noticias?page=1&perPage=4— todos recebem o mesmo formato já pronto.
public interface INoticiasAdapterClient
{
Task<AdapterResponse> ListarAsync(int page = 1, int perPage = 4);
}
public sealed class AdapterResponse
{
public IEnumerable<AdapterItem> Items { get; set; }
public AdapterMeta Meta { get; set; }
}
public sealed class AdapterItem
{
public string Titulo { get; set; }
public string ResumoHtml { get; set; }
public string Link { get; set; }
public string Imagem { get; set; }
public string Fonte { get; set; }
}
public sealed class AdapterMeta { public int Page { get; set; } public int PerPage { get; set; } }
public sealed class NoticiasAdapterClient : INoticiasAdapterClient
{
private readonly HttpClient _http;
public NoticiasAdapterClient(HttpClient http) => _http = http;
public async Task<AdapterResponse> ListarAsync(int page = 1, int perPage = 4)
{
var url = $"/api/noticias?page={page}&perPage={perPage}";
var res = await _http.GetAsync(url);
res.EnsureSuccessStatusCode();
return await res.Content.ReadFromJsonAsync<AdapterResponse>() ?? new();
}
}
Registro no Program.cs (ou Startup)
builder.Services.AddHttpClient<INoticiasAdapterClient, NoticiasAdapterClient>(c =>
{
c.BaseAddress = new Uri(Environment.GetEnvironmentVariable("ADAPTER_BASE_URL")!);
});
Controller MVC usando o Adapter
public class HomeController : Controller
{
private readonly INoticiasAdapterClient _adapter;
public HomeController(INoticiasAdapterClient adapter) { _adapter = adapter; }
[HttpGet]
public async Task<IActionResult> Noticias()
{
var resp = await _adapter.ListarAsync(page: 1, perPage: 4);
// Mapear para a mesma ViewModel da integração direta, se preferir
var vm = resp.Items.Select(i => new NoticiaViewModel {
Titulo = i.Titulo,
ResumoHtml = i.ResumoHtml,
Link = i.Link,
UrlImagem = i.Imagem
}).ToList();
return PartialView("_Noticias", vm);
}
}
per_page (ex.: máx. 20)._fields para buscar só o necessário.loading="lazy" nas imagens, definir width/height quando possível.alt), contraste, foco visível, teclado.<script>
async function carregarNoticias() {
const url = "https://rondonia.ro.gov.br/wp-json/wp/v2/posts?per_page=4&_fields=title,excerpt,link,yoast_head_json.og_image";
const resp = await fetch(url);
const data = await resp.json();
data.forEach(n => {
console.log(n.title.rendered, n.excerpt.rendered, n.link, n.yoast_head_json?.og_image?.[0]?.url);
});
}
carregarNoticias();
</script>
app.get("/api/noticias", async (req, res) => {
const page = Number(req.query.page || 1);
const perPage = Math.min(Number(req.query.perPage || 4), 20);
const url = `${process.env.WP_BASE_URL}/wp-json/wp/v2/posts?per_page=${perPage}&page=${page}&_fields=title,excerpt,link,yoast_head_json.og_image`;
const r = await fetch(url, { headers: { "User-Agent": "Portal-Integrador/1.0" }});
if (!r.ok) return res.json({ items: [] });
const data = await r.json();
const items = data.map(c => ({
titulo: c.title?.rendered ?? "",
resumoHtml: c.excerpt?.rendered ?? "",
link: c.link,
imagem: c.yoast_head_json?.og_image?.[0]?.url ?? null,
fonte: "Portal do Governo"
}));
res.json({ items, meta: { page, perPage }});
});
$perPage = min(intval($_GET['perPage'] ?? 4), 20);
$page = max(intval($_GET['page'] ?? 1), 1);
$url = getenv('WP_BASE_URL')."/wp-json/wp/v2/posts?per_page=$perPage&page=$page&_fields=title,excerpt,link,yoast_head_json.og_image";
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["User-Agent: Portal-Integrador/1.0"]]);
$resp = curl_exec($ch); $http = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch);
if ($http !== 200) { echo json_encode(["items" => []]); exit; }
$data = json_decode($resp, true) ?? [];
$items = array_map(fn($c) => [
"titulo" => html_entity_decode($c["title"]["rendered"] ?? ""),
"resumoHtml" => $c["excerpt"]["rendered"] ?? "",
"link" => $c["link"] ?? "#",
"imagem" => $c["yoast_head_json"]["og_image"][0]["url"] ?? null,
"fonte" => "Portal do Governo"
], $data);
echo json_encode(["items" => $items, "meta" => ["page" => $page, "perPage" => $perPage]]);
import os, requests, html
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.get("/api/noticias")
def noticias():
per_page = min(int(request.args.get("perPage", 4)), 20)
page = max(int(request.args.get("page", 1)), 1)
url = f"{os.getenv('WP_BASE_URL')}/wp-json/wp/v2/posts?per_page={per_page}&page={page}&_fields=title,excerpt,link,yoast_head_json.og_image"
r = requests.get(url, headers={"User-Agent": "Portal-Integrador/1.0"}, timeout=8)
if r.status_code != 200: return jsonify({"items": []})
data = r.json()
items = [{
"titulo": html.unescape((c.get("title") or {}).get("rendered") or ""),
"resumoHtml": (c.get("excerpt") or {}).get("rendered"),
"link": c.get("link"),
"imagem": ((c.get("yoast_head_json") or {}).get("og_image") or [{}])[0].get("url"),
"fonte": "Portal do Governo"
} for c in data]
return jsonify({ "items": items, "meta": { "page": page, "perPage": per_page }})
O estudo mostrou que a integração da API de notícias é possível e traz benefícios importantes para o Portal do Governo. Essa solução facilita o acesso às informações, garante maior agilidade na atualização de conteúdos e permite que outros sistemas também possam compartilhar as mesmas notícias de forma padronizada.
Com isso, o Portal passa a ter uma base mais confiável e organizada, fortalecendo a transparência e a comunicação com os cidadãos. Além disso, o modelo criado pode ser reaproveitado em outros projetos do governo, tornando o processo mais prático e sustentável a longo prazo.