diff --git a/.browserslistrc b/.browserslistrc
new file mode 100644
index 0000000..a710dfe
--- /dev/null
+++ b/.browserslistrc
@@ -0,0 +1,7 @@
+chrome >= 90
+firefox >= 90
+edge >= 90
+safari >= 15
+ios_saf >= 15
+and_chr >= 90
+not dead
diff --git a/app/favicon.ico b/app/favicon.ico
new file mode 100644
index 0000000..ab9e2da
Binary files /dev/null and b/app/favicon.ico differ
diff --git a/app/frameworks/[key]/page.jsx b/app/frameworks/[key]/page.jsx
index afb3237..0338a62 100644
--- a/app/frameworks/[key]/page.jsx
+++ b/app/frameworks/[key]/page.jsx
@@ -1,4 +1,4 @@
-import { paginaMeta } from "@/lib/seo";
+import { paginaMeta, jsonLd } from "@/lib/seo";
import Link from "next/link";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
@@ -33,6 +33,22 @@ export default async function FrameworkDetail({ params }) {
return (
+ {/* Ficha técnica do framework e trilha de navegação para o Google */}
+
+
+
diff --git a/app/glossario/page.jsx b/app/glossario/page.jsx
index ee5de6d..a11d793 100644
--- a/app/glossario/page.jsx
+++ b/app/glossario/page.jsx
@@ -374,6 +374,15 @@ export default function Glossario() {
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.glossario(categorias)) }}
/>
+
diff --git a/app/layout.jsx b/app/layout.jsx
index 78c0c2f..f40c349 100644
--- a/app/layout.jsx
+++ b/app/layout.jsx
@@ -72,7 +72,20 @@ export const metadata = {
},
},
- icons: { icon: "/marca/espdocs-marca.svg", apple: "/marca/espdocs-marca.svg" },
+ icons: {
+ icon: [
+ { url: "/favicon.ico", sizes: "any" },
+ { url: "/icon.svg", type: "image/svg+xml" },
+ { url: "/favicon-48x48.png", sizes: "48x48", type: "image/png" },
+ { url: "/icon-192.png", sizes: "192x192", type: "image/png" },
+ { url: "/icon-512.png", sizes: "512x512", type: "image/png" },
+ ],
+ apple: [
+ { url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" },
+ ],
+ shortcut: "/favicon.ico",
+ },
+ manifest: "/site.webmanifest",
formatDetection: { telephone: false, address: false, email: false },
};
diff --git a/app/page.jsx b/app/page.jsx
index 96f44f9..bc66668 100644
--- a/app/page.jsx
+++ b/app/page.jsx
@@ -1,8 +1,7 @@
-"use client";
-
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import Link from "next/link";
+import Image from "next/image";
import {
SlidersHorizontal,
ShoppingBag,
@@ -188,10 +187,12 @@ export default function Home() {
rel="noopener noreferrer"
className="group mb-6"
>
-
diff --git a/app/sobre/page.jsx b/app/sobre/page.jsx
index c5ed414..91b79f8 100644
--- a/app/sobre/page.jsx
+++ b/app/sobre/page.jsx
@@ -2,6 +2,7 @@ import { paginaMeta } from "@/lib/seo";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import Link from "next/link";
+import Image from "next/image";
import {
Languages,
GitCompare,
@@ -172,9 +173,11 @@ export default function Sobre() {
-
diff --git a/components/Header.jsx b/components/Header.jsx
index fdb301a..0241908 100644
--- a/components/Header.jsx
+++ b/components/Header.jsx
@@ -68,7 +68,7 @@ export default function Header() {
{/* eslint-disable-next-line @next/next/no-img-element */}
-
+
ESPDocs
diff --git a/lib/seo.js b/lib/seo.js
index 1f1093d..d77310c 100644
--- a/lib/seo.js
+++ b/lib/seo.js
@@ -91,31 +91,95 @@ export const jsonLd = {
};
},
- /** Ficha técnica de uma série, com as especificações como propriedades. */
+ /** Documentação e ficha técnica da série como TechArticle para o Google. */
serie(chave, serie) {
const prop = (nome, valor) =>
valor ? { "@type": "PropertyValue", name: nome, value: String(valor) } : null;
+ const propriedades = [
+ prop("Fabricante", "Espressif Systems"),
+ prop("Arquitetura", serie.arquitetura),
+ prop("Núcleos", serie.nucleos),
+ prop("Frequência máxima", serie.frequencia),
+ prop("SRAM", serie.memoria_sram),
+ prop("RTC SRAM", serie.memoria_sram_rtc),
+ prop("Flash suportada", serie.flash_suportada),
+ prop("PSRAM suportada", serie.psram_suportada),
+ prop("GPIO", serie.gpio),
+ prop("Wi-Fi", serie.wifi),
+ prop("Bluetooth", serie.bluetooth),
+ prop("Zigbee / Thread / 802.15.4", serie.zigbee_thread),
+ prop("Encapsulamento", serie.encapsulamento),
+ prop("ESP-IDF mínimo", serie.esp_idf_minimo),
+ ].filter(Boolean);
+
return {
"@context": "https://schema.org",
- "@type": "Product",
- "@id": `${SITE.url}/series/${chave}#produto`,
- name: serie.nome_completo || chave,
+ "@type": "TechArticle",
+ "@id": `${SITE.url}/series/${chave}#artigo`,
+ headline: `${serie.nome_completo || chave}: especificações e pinagem`,
+ name: `${serie.nome_completo || chave} - Documentação Técnica e Especificações`,
description: serie.descricao,
- category: "Microcontrolador",
- brand: { "@type": "Brand", name: "Espressif Systems" },
+ inLanguage: "pt-BR",
+ mainEntityOfPage: `${SITE.url}/series/${chave}`,
url: `${SITE.url}/series/${chave}`,
- additionalProperty: [
- prop("Arquitetura", serie.arquitetura),
- prop("Núcleos", serie.nucleos),
- prop("Frequência máxima", serie.frequencia),
- prop("SRAM", serie.memoria_sram),
- prop("GPIO", serie.gpio),
- prop("Wi-Fi", serie.wifi),
- prop("Bluetooth", serie.bluetooth),
- prop("Encapsulamento", serie.encapsulamento),
- prop("ESP-IDF mínimo", serie.esp_idf_minimo),
- ].filter(Boolean),
+ author: {
+ "@type": "Person",
+ name: SITE.autor,
+ url: SITE.autorUrl,
+ },
+ publisher: {
+ "@type": "Organization",
+ name: "Ciência Embarcada",
+ url: SITE.blog,
+ logo: {
+ "@type": "ImageObject",
+ url: `${SITE.url}/marca/espdocs-marca.svg`,
+ },
+ },
+ about: {
+ "@type": "Thing",
+ name: serie.nome_completo || chave,
+ description: serie.descricao,
+ additionalProperty: propriedades,
+ },
+ };
+ },
+
+ /** Documentação técnica de framework. */
+ framework(chave, framework) {
+ return {
+ "@context": "https://schema.org",
+ "@type": "TechArticle",
+ "@id": `${SITE.url}/frameworks/${encodeURIComponent(chave)}#artigo`,
+ headline: `${framework.nome} para ESP32: Guia e Documentação`,
+ name: `${framework.nome_completo || framework.nome} para ESP32`,
+ description: framework.descricao,
+ inLanguage: "pt-BR",
+ mainEntityOfPage: `${SITE.url}/frameworks/${encodeURIComponent(chave)}`,
+ url: `${SITE.url}/frameworks/${encodeURIComponent(chave)}`,
+ author: {
+ "@type": "Person",
+ name: SITE.autor,
+ url: SITE.autorUrl,
+ },
+ publisher: {
+ "@type": "Organization",
+ name: "Ciência Embarcada",
+ url: SITE.blog,
+ logo: {
+ "@type": "ImageObject",
+ url: `${SITE.url}/marca/espdocs-marca.svg`,
+ },
+ },
+ about: {
+ "@type": "SoftwareApplication",
+ name: framework.nome_completo || framework.nome,
+ applicationCategory: "DeveloperApplication",
+ operatingSystem: "Embedded",
+ description: framework.descricao,
+ url: framework.documentacao || `${SITE.url}/frameworks/${encodeURIComponent(chave)}`,
+ },
};
},
diff --git a/next.config.mjs b/next.config.mjs
index 4678774..e32dec3 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -1,4 +1,16 @@
/** @type {import('next').NextConfig} */
-const nextConfig = {};
+const nextConfig = {
+ experimental: {
+ optimizePackageImports: [
+ "lucide-react",
+ "@mui/material",
+ "@mui/icons-material",
+ "framer-motion",
+ ],
+ },
+ images: {
+ formats: ["image/avif", "image/webp"],
+ },
+};
export default nextConfig;
diff --git a/package.json b/package.json
index adf5ff6..160fd99 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,7 @@
"build": "next build --turbopack",
"start": "next start",
"lint": "eslint",
+ "favicons": "node scripts/generate-favicons.mjs",
"auditar-temas": "node scripts/auditar-temas.mjs",
"slides-pdf": "node scripts/slides-pdf.mjs"
},
diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png
new file mode 100644
index 0000000..1d90bd0
Binary files /dev/null and b/public/apple-touch-icon.png differ
diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png
new file mode 100644
index 0000000..3382e21
Binary files /dev/null and b/public/favicon-16x16.png differ
diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png
new file mode 100644
index 0000000..30e9849
Binary files /dev/null and b/public/favicon-32x32.png differ
diff --git a/public/favicon-48x48.png b/public/favicon-48x48.png
new file mode 100644
index 0000000..2817f0e
Binary files /dev/null and b/public/favicon-48x48.png differ
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..ab9e2da
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/icon-192.png b/public/icon-192.png
new file mode 100644
index 0000000..34a65cc
Binary files /dev/null and b/public/icon-192.png differ
diff --git a/public/icon-512.png b/public/icon-512.png
new file mode 100644
index 0000000..39fcaaa
Binary files /dev/null and b/public/icon-512.png differ
diff --git a/public/site.webmanifest b/public/site.webmanifest
new file mode 100644
index 0000000..2691d43
--- /dev/null
+++ b/public/site.webmanifest
@@ -0,0 +1,26 @@
+{
+ "name": "ESPDocs",
+ "short_name": "ESPDocs",
+ "description": "Documentação do ecossistema ESP32 em português brasileiro",
+ "start_url": "/",
+ "display": "standalone",
+ "background_color": "#020617",
+ "theme_color": "#8b5cf6",
+ "icons": [
+ {
+ "src": "/favicon-48x48.png",
+ "sizes": "48x48",
+ "type": "image/png"
+ },
+ {
+ "src": "/icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/scripts/generate-favicons.mjs b/scripts/generate-favicons.mjs
new file mode 100644
index 0000000..b0afe9f
--- /dev/null
+++ b/scripts/generate-favicons.mjs
@@ -0,0 +1,130 @@
+import fs from 'fs';
+import path from 'path';
+import puppeteer from 'puppeteer';
+
+const svgPath = path.resolve('app/icon.svg');
+const svgContent = fs.readFileSync(svgPath, 'utf8');
+
+function createIco(pngBuffers) {
+ // pngBuffers: array of { width, height, buffer }
+ const count = pngBuffers.length;
+ const headerSize = 6;
+ const dirEntrySize = 16;
+ let offset = headerSize + dirEntrySize * count;
+
+ const header = Buffer.alloc(headerSize);
+ header.writeUInt16LE(0, 0); // reserved
+ header.writeUInt16LE(1, 2); // ICO type
+ header.writeUInt16LE(count, 4); // count
+
+ const entries = [];
+ for (const img of pngBuffers) {
+ const entry = Buffer.alloc(dirEntrySize);
+ entry.writeUInt8(img.width >= 256 ? 0 : img.width, 0);
+ entry.writeUInt8(img.height >= 256 ? 0 : img.height, 1);
+ entry.writeUInt8(0, 2); // colors
+ entry.writeUInt8(0, 3); // reserved
+ entry.writeUInt16LE(1, 4); // planes
+ entry.writeUInt16LE(32, 6); // bpp
+ entry.writeUInt32LE(img.buffer.length, 8); // size
+ entry.writeUInt32LE(offset, 12); // offset
+ entries.push(entry);
+ offset += img.buffer.length;
+ }
+
+ return Buffer.concat([header, ...entries, ...pngBuffers.map(b => b.buffer)]);
+}
+
+async function run() {
+ const browser = await puppeteer.launch({
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
+ });
+ const page = await browser.newPage();
+
+ const html = `
+
+
+
+
+ ${svgContent}
+ `;
+
+ await page.setContent(html);
+
+ const sizes = [16, 32, 48, 96, 180, 192, 512];
+ const rendered = {};
+
+ for (const size of sizes) {
+ await page.setViewport({ width: size, height: size, deviceScaleFactor: 1 });
+ const buffer = await page.screenshot({
+ type: 'png',
+ omitBackground: true,
+ clip: { x: 0, y: 0, width: size, height: size }
+ });
+ rendered[size] = buffer;
+ console.log(`Rendered ${size}x${size}`);
+ }
+
+ await browser.close();
+
+ // Create ICO from 16, 32, 48
+ const icoBuffer = createIco([
+ { width: 16, height: 16, buffer: rendered[16] },
+ { width: 32, height: 32, buffer: rendered[32] },
+ { width: 48, height: 48, buffer: rendered[48] }
+ ]);
+
+ // Write favicon.ico to public and app
+ fs.writeFileSync('public/favicon.ico', icoBuffer);
+ fs.writeFileSync('app/favicon.ico', icoBuffer);
+ console.log('Saved favicon.ico to public/ and app/');
+
+ // Write PNG favicons to public
+ fs.writeFileSync('public/favicon-48x48.png', rendered[48]);
+ fs.writeFileSync('public/favicon-32x32.png', rendered[32]);
+ fs.writeFileSync('public/favicon-16x16.png', rendered[16]);
+ fs.writeFileSync('public/apple-touch-icon.png', rendered[180]);
+ fs.writeFileSync('public/icon-192.png', rendered[192]);
+ fs.writeFileSync('public/icon-512.png', rendered[512]);
+ console.log('Saved PNG icons to public/');
+
+ // Create site.webmanifest
+ const manifest = {
+ name: "ESPDocs",
+ short_name: "ESPDocs",
+ description: "Documentação do ecossistema ESP32 em português brasileiro",
+ start_url: "/",
+ display: "standalone",
+ background_color: "#020617",
+ theme_color: "#8b5cf6",
+ icons: [
+ {
+ src: "/favicon-48x48.png",
+ sizes: "48x48",
+ type: "image/png"
+ },
+ {
+ src: "/icon-192.png",
+ sizes: "192x192",
+ type: "image/png"
+ },
+ {
+ src: "/icon-512.png",
+ sizes: "512x512",
+ type: "image/png"
+ }
+ ]
+ };
+
+ fs.writeFileSync('public/site.webmanifest', JSON.stringify(manifest, null, 2));
+ console.log('Saved site.webmanifest to public/');
+}
+
+run().catch(err => {
+ console.error(err);
+ process.exit(1);
+});