Atualizado em 12/08/2026 para o WingedSwift 2.0
Este artigo foi escrito originalmente na versão 1.3.3 e todos os exemplos foram revisados e recompilados contra a 2.0. O que mudou na prática:
- Swift 6.0+ / Xcode 16+ passou a ser obrigatório (a biblioteca compila em language mode 6).
- **
Document** é o novo topo da página: ele é dono do<!DOCTYPE html>e do<html lang="…">, no lugar de montarhtml { Head(...) Body(...) }na mão. - **
RenderOptions** substitui o estado global de renderização —render(.compact)/render(.pretty)em vez derender(pretty:)eHTMLTag.xhtmlSelfClosing. - Result builders em todo container:
Div { ... }comif,foremapdentro, sem precisar dochildren: [...]. - **CLI
winged**:winged new,winged buildewinged serve --watch.
Nada foi removido na 2.0 — código escrito na 1.x continua compilando e gerando o mesmo HTML. Os membros antigos estão marcados como deprecated e saem na 3.0, então vale migrar.
Como desenvolvedor iOS, já me acostumei a trabalhar com Swift no dia a dia. Mas você já pensou em usar Swift para criar sites estáticos? É exatamente isso que o WingedSwift permite fazer - uma biblioteca Swift que gera HTML usando uma DSL (Domain-Specific Language) elegante e type-safe.
Neste artigo, vou mostrar como criar uma landing page moderna e responsiva usando WingedSwift combinado com Tailwind CSS, seguindo a mesma abordagem que usei para criar o site do NFC Forge.
O que vamos construir
Vamos criar uma landing page completa para um aplicativo, incluindo:
- Hero section com screenshot do app
- Seção de features com ícones
- Design responsivo e moderno
- Integração com Google Analytics
- Build automatizado com Tailwind CSS
Por que WingedSwift?
Antes de mergulharmos no código, deixa eu explicar por que desenvolvi WingedSwift e estou trabalhando com ele em meus sites:
- Type Safety: Erros de sintaxe são capturados em tempo de compilação
- Familiaridade: Se você já conhece Swift, vai se sentir em casa
- Composição: Código reutilizável e modular
- Performance: Gera HTML estático, super rápido para servir
A muitos e muitos anos atrás em uma galaxia não tão distânte, eu havia desenvolvido algo similar mas com PHP. Na época tinha empresa de desenvolvimento de sites, sistemas web e aplicativos, e isso me ajudou muito a agilizar o trabalho pois conseguia construir diversos templates para sites e sistemas. Com meu foco ficando exclusivamente em mobile resolvi fazer o mesmo com uma linguagem quem me apaixonei que foi o Swift.
Bora codar...
Atalho: `winged new`
A 2.0 trouxe uma CLI. Se você quiser pular a montagem manual do projeto:
git clone https://github.com/micheltlutz/Winged-Swift.git
cd Winged-Swift && swift build -c release
# o binário fica em .build/release/winged
winged new MeuSite --tailwind
cd MeuSite
winged build # gera em dist/
winged serve --watch # http://localhost:8000, rebuild a cada alteraçãoAinda assim vale seguir o passo a passo abaixo: é ele que mostra o que a CLI faz por baixo, e é o que você vai precisar entender quando o site sair do esqueleto.
Um detalhe que economiza tempo: winged build espera o layout que o winged new cria — um
produto executável declarado no Package.swift, que recebe o diretório de saída como
primeiro argumento. O projeto montado à mão neste artigo escreve direto em Config.outputDir
e é construído pelo build.sh, então ele usa a CLI só para servir (mostro isso no final).
Estrutura do Projeto
Vamos começar criando a estrutura básica do projeto:
meu-site/
├── Sources/
│ └── MeuSite/
│ ├── MeuSite.swift # Gerador principal
│ ├── Config.swift # Configurações
│ └── Models.swift # Modelos de dados
├── assets/
│ ├── images/
│ ├── css/
│ │ └── tailwind.input.css
│ └── js/
│ └── script.js
├── output/ # Site gerado
├── Package.swift
├── package.json
├── tailwind.config.ts
└── build.shPasso 1: Configurando o Package.swift
Primeiro, vamos criar o arquivo Package.swift para definir as dependências:
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "MeuSite",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(url: "https://github.com/micheltlutz/Winged-Swift.git", from: "2.0.0")
],
targets: [
.executableTarget(
name: "MeuSite",
dependencies: [
.product(name: "WingedSwift", package: "Winged-Swift")
]
),
]
)O swift-tools-version do seu pacote não precisa ser 6.0 — um pacote em 5.9 consegue depender do WingedSwift 2.0 desde que o compilador seja 6.0+. Aqui uso 6.0 porque projeto novo não tem motivo para nascer velho.Passo 2: Criando os Modelos de Dados
Vamos criar modelos simples para representar nosso conteúdo. Em vez de usar um CMS externo como Strapi, vamos usar estruturas Swift e opcionalmente carregar dados de um JSON.
// Sources/MeuSite/Models.swift
import Foundation
struct Feature: Codable {
let id: Int
let title: String
let description: String
let icon: String
let order: Int
}
struct AppConfig: Codable {
let name: String
let description: String
let shortDescription: String
let appStoreURL: String
let screenshot: String
let logo: String
let features: [Feature]
}
// Configurações do site
struct Config {
static let outputDir = "./output"
static let siteName = "Meu App"
static let siteDescription = "Descrição do meu aplicativo"
static let siteURL = "https://meuapp.com"
static let author = "Seu Nome"
static let accentColor = "#99cc00"
static let googleAnalyticsID = "G-XXXXXXXXXX"
}Passo 3: Carregando Dados de JSON (Opcional)
Se preferir manter o conteúdo em um arquivo JSON, podemos criar um carregador:
// Sources/MeuSite/DataLoader.swift
import Foundation
struct DataLoader {
static func loadAppConfig(from path: String = "./content.json") -> AppConfig? {
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)),
let config = try? JSONDecoder().decode(AppConfig.self, from: data) else {
return nil
}
return config
}
static func getDefaultConfig() -> AppConfig {
return AppConfig(
name: "Meu App",
description: "Descrição completa do aplicativo",
shortDescription: "Uma descrição curta e impactante",
appStoreURL: "https://apps.apple.com/app/meu-app",
screenshot: "./assets/images/screenshot.png",
logo: "./assets/images/logo.png",
features: [
Feature(
id: 1,
title: "Feature 1",
description: "Descrição detalhada da primeira feature",
icon: "star.svg",
order: 1
),
Feature(
id: 2,
title: "Feature 2",
description: "Descrição detalhada da segunda feature",
icon: "heart.svg",
order: 2
)
]
)
}
}Passo 4: Criando o Gerador Principal
Agora vamos criar o arquivo principal que gera o HTML usando WingedSwift, em alguns momentos vou usar algumas formas diferentes de escrever uma Tag HTML para exemplificar a flexibilidade que o WingedSwift tem:
// Sources/MeuSite/MeuSite.swift
import Foundation
import WingedSwift
@main
struct MeuSite {
static func main() async throws {
print("Gerando site...")
// Carregar dados (JSON ou padrão)
let config = DataLoader.loadAppConfig() ?? DataLoader.getDefaultConfig()
let generator = StaticSiteGenerator(outputDirectory: Config.outputDir)
let mainPage = createMainPage(config: config)
try generator.generate(
document: mainPage,
to: "index.html",
options: .compact
)
print("Site gerado com sucesso em: \(Config.outputDir)")
}
static func createMainPage(config: AppConfig) -> Document {
return Document(
lang: "pt-BR",
head: Head {
Meta(charset: "UTF-8")
Meta(name: "viewport", content: "width=device-width, initial-scale=1.0")
Title(content: "\(config.name) - \(Config.siteDescription)")
// SEO Meta Tags
Meta(name: "description", content: config.description)
Meta(name: "keywords", content: "app, ios, swift")
Meta(name: "author", content: Config.author)
// Open Graph — repare no `property:`, e não `name:`: é o que o
// LinkedIn e o WhatsApp leem para montar o preview do link.
Meta(property: "og:title", content: config.name)
Meta(property: "og:description", content: config.description)
Meta(property: "og:type", content: "website")
Meta(property: "og:url", content: Config.siteURL)
Meta(property: "og:image", content: "\(Config.siteURL)\(config.logo.dropFirst(1))")
// Tailwind CSS
Link(href: "./assets/css/style.css", rel: "stylesheet")
// Google Analytics
Script(attributes: [
Attribute.boolean("async"),
Attribute(key: "src", value: "https://www.googletagmanager.com/gtag/js?id=\(Config.googleAnalyticsID)")
])
Script(attributes: [], content: """
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '\(Config.googleAnalyticsID)');
""")
},
body: Body {
createHeader(config: config)
createHeroSection(config: config)
createFeaturesSection(features: config.features)
createFooter()
}
.addClass("bg-black text-white min-h-screen")
)
}
static func createHeader(config: AppConfig) -> HTMLTag {
return Header {
Div {
Img(src: config.logo, alt: "\(config.name) Logo")
.addClass("w-12 h-12 rounded-xl")
Nav {
A(href: "#features", content: "Features")
.addClass("text-white hover:text-accent transition-colors")
}
.addClass("ml-auto flex gap-6")
}
.addClass("container mx-auto px-6 py-4 flex items-center")
}
.addClass("bg-black border-b border-gray-800")
}
static func createHeroSection(config: AppConfig) -> HTMLTag {
return Section(children: [
Div(children: [
Div(children: [
createIPhoneMockup(screenshot: config.screenshot)
])
.addClass("flex justify-center"),
Div(children: [
Div(children: [
Img(src: config.logo, alt: "\(config.name) Icon")
.addClass("w-20 h-20 rounded-2xl shadow-lg"),
Div(children: [
H1(content: config.name)
.addClass("text-5xl font-bold text-white mb-1"),
Span(content: "App")
.addClass("text-gray-400 text-xl")
])
.addClass("ml-6")
])
.addClass("flex items-center mb-6"),
P(content: config.shortDescription)
.addClass("text-gray-300 text-lg leading-relaxed mb-8 max-w-lg"),
HTMLTag("a", attributes: [
Attribute(key: "href", value: config.appStoreURL),
Attribute(key: "target", value: "_blank"),
Attribute(key: "rel", value: "noopener noreferrer")
], children: [
Img(src: "./assets/images/app-store-badge.svg", alt: "Download on the App Store")
.addClass("h-14")
])
.addClass("inline-block")
])
.addClass("flex flex-col justify-center lg:pl-12")
])
.addClass("container mx-auto px-6 py-16 lg:py-24 grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-center")
])
.addClass("bg-black")
}
static func createIPhoneMockup(screenshot: String) -> HTMLTag {
return Div {
Img(src: screenshot, alt: "App Screenshot")
.addClass("w-full h-full object-cover rounded-[2.5rem]")
}
.addClass("w-80 h-[694px] bg-black rounded-[3rem] border-8 border-gray-800 shadow-2xl overflow-hidden")
}
static func createFeaturesSection(features: [Feature]) -> HTMLTag {
let sortedFeatures = features.sorted { $0.order < $1.order }
return Section {
Div {
H2(content: "Recursos Principais")
.addClass("text-4xl font-bold text-center mb-16 text-white")
// `map` dentro do builder: na 2.0 isso compila sem ambiguidade,
// não precisa montar o array antes.
Div {
sortedFeatures.map { createFeatureCard(feature: $0) }
}
.addClass("grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8")
}
.addClass("container mx-auto px-6 py-16")
}
.addClass("bg-black")
}
static func createFeatureCard(feature: Feature) -> HTMLTag {
return Div {
Div {
Div {
Img(src: "./assets/images/icons/\(feature.icon)", alt: feature.title)
.addClass("w-12 h-12")
}
.addClass("flex justify-center mb-4")
H3(content: feature.title)
.addClass("text-xl font-bold text-white mb-3")
P(content: feature.description)
.addClass("text-gray-300 leading-relaxed flex-grow")
}
.addClass("text-center p-8 bg-gray-800 rounded-lg hover:bg-gray-700 transition-colors flex flex-col h-full")
}
.addClass("h-full")
}
static func createFooter() -> HTMLTag {
return Footer {
Div {
P(content: "© 2026 \(Config.author). Todos os direitos reservados.")
.addClass("text-gray-400 text-center")
}
.addClass("container mx-auto px-6 py-8")
}
.addClass("bg-black border-t border-gray-800")
}
}Repare que a createHeroSection acima continua na forma antiga, com children: [...].
É de propósito: a forma em array não foi descontinuada e as duas convivem no mesmo
arquivo. Onde tem muito encadeamento de .addClass entre irmãos, o array com vírgulas
às vezes lê melhor; onde tem if ou map, o builder ganha de longe.
Passo 5: Configurando Tailwind CSS
Crie o arquivo tailwind.config.ts:
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./Sources/**/*.swift',
'./output/**/*.html',
],
darkMode: 'class',
theme: {
extend: {
colors: {
primary: '#000000',
secondary: '#1a1a1a',
accent: '#99cc00',
'accent-light': '#b8e633',
text: '#ffffff',
'text-secondary': '#a0a0a0',
},
},
},
plugins: [],
}
export default configE o arquivo assets/css/tailwind.input.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
-webkit-font-smoothing: antialiased;
}
}
@layer components {
.container {
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8;
}
}Passo 6: Criando o Script de Build
O script build.sh automatiza todo o processo:
#!/bin/bash
echo "🚀 Building site..."
# Limpar builds anteriores
rm -rf .build output
# Build Swift
echo "Building Swift project..."
swift build
if [ $? -ne 0 ]; then
echo "❌ Swift build failed!"
exit 1
fi
# Criar diretório output antes de gerar os arquivos
echo "Creating output directory..."
mkdir -p output/assets/images/icons
mkdir -p output/assets/css
mkdir -p output/assets/js
# Executar gerador (gera os arquivos HTML em output/)
echo "Generating HTML..."
.build/debug/MeuSite
if [ $? -ne 0 ]; then
echo "❌ Site generation failed!"
exit 1
fi
# Copiar assets
echo "Copying assets..."
cp -r assets/images/* output/assets/images/ 2>/dev/null || true
cp -r assets/js/* output/assets/js/ 2>/dev/null || true
# Instalar Tailwind CSS se necessário
if [ ! -d "node_modules" ]; then
echo "Installing Tailwind CSS..."
npm install
fi
# Compilar Tailwind CSS
echo "Compiling Tailwind CSS..."
# Garantir que o diretório CSS existe
mkdir -p ./output/assets/css
if [ -f "node_modules/.bin/tailwindcss" ]; then
./node_modules/.bin/tailwindcss -i ./assets/css/tailwind.input.css -o ./output/assets/css/style.css --minify
else
npx --yes tailwindcss@latest -i ./assets/css/tailwind.input.css -o ./output/assets/css/style.css --minify
fi
echo "✅ Build complete! Site available in output/"Passo 7: Criando o package.json
{
"name": "meu-site",
"version": "1.0.0",
"private": true,
"scripts": {
"build:css": "tailwindcss -i ./assets/css/tailwind.input.css -o ./output/assets/css/style.css --minify"
},
"devDependencies": {
"tailwindcss": "^3.4.18"
}
}Passo 8: Arquivo JSON de Conteúdo (Opcional)
Se preferir usar JSON, crie um arquivo content.json:
{
"name": "Meu App",
"description": "Descrição completa do aplicativo",
"shortDescription": "Uma descrição curta e impactante do que o app faz",
"appStoreURL": "https://apps.apple.com/app/meu-app",
"screenshot": "./assets/images/screenshot.png",
"logo": "./assets/images/logo.png",
"features": [
{
"id": 1,
"title": "Feature 1",
"description": "Descrição detalhada da primeira feature",
"icon": "star.svg",
"order": 1
},
{
"id": 2,
"title": "Feature 2",
"description": "Descrição detalhada da segunda feature",
"icon": "heart.svg",
"order": 2
}
]
}Executando o Build
Agora você pode gerar o site:
# Dar permissão de execução
chmod +x build.sh
# Executar build
./build.sh
# Servir localmente
cd output && python3 -m http.server 8000Ou, sem depender do Python, com o servidor embutido na CLI da 2.0:
winged serve --output output --no-buildO --no-build é o que faz isso funcionar aqui: ele pula o build da CLI e serve o que o
build.sh já gerou.
Acesse http://localhost:8000 para ver o site gerado.
O que temos até aqui?
- Controle Total: Todo o código está no seu controle, sem dependências externas complexas
- Type Safety: Erros são detectados em tempo de compilação
- Performance: HTML estático é extremamente rápido
- Flexibilidade: Fácil de integrar com CI/CD e deploy em qualquer serviço de hospedagem estática
- Manutenibilidade: Código Swift é fácil de manter e testar
Deploy
O diretório output/ contém o site completo pronto para deploy. Você pode:
- Fazer deploy no AWS Amplify
- Fazer deploy no Vercel
- Fazer deploy no GitHub Pages
- Fazer deploy no Fly.io
- Fazer deploy em qualquer servidor web estático
Concluindo
WingedSwift oferece uma maneira elegante e type-safe de gerar sites estáticos usando Swift. Combinado com Tailwind CSS, você pode criar landing pages modernas e responsivas sem precisar aprender um novo framework ou linguagem.
A abordagem de manter o conteúdo no código (ou em JSON) torna o projeto mais simples e direto, especialmente para projetos menores ou quando você não precisa de um CMS completo.
Se você já conhece Swift e está procurando uma alternativa moderna para gerar sites estáticos, definitivamente vale a pena experimentar o WingedSwift.
Recursos
- WingedSwift no GitHub
- Guia de migração 1.x → 2.0
- CHANGELOG da 2.0
- Tailwind CSS Documentation
- Exemplo completo: NFC Forge Site
- Exemplo completo: RideKeeper
- Exemplo completo: ML3dPrint
Arquivo .gitignore
Para manter o repositório limpo e evitar commitar arquivos desnecessários, crie um arquivo .gitignore na raiz do projeto:
# Swift
.build/
*.xcodeproj
*.xcworkspace
.swiftpm/
Package.resolved
# Build output (site gerado)
# IMPORTANTE: A pasta output/ NÃO deve ser ignorada se você fizer deploy
# direto do repositório (como AWS Amplify). Ela contém o site estático.
# output/ ← Descomente apenas se não precisar commitar o output
# Node.js
node_modules/
package-lock.json
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# macOS
.DS_Store
.AppleDouble
.LSOverride
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# Arquivos de conteúdo (opcional)
# Se você usar JSON para conteúdo e não quiser versionar:
# content.json
# content.local.json**Nota importante sobre o diretório output/**: Se você fizer deploy direto do repositório (como no AWS Amplify), você precisará commitar a pasta output/ com o site gerado. Se você fizer build durante o deploy, pode ignorar essa pasta.


