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 montar html { Head(...) Body(...) } na mão.
  • **RenderOptions** substitui o estado global de renderização — render(.compact) / render(.pretty) em vez de render(pretty:) e HTMLTag.xhtmlSelfClosing.
  • Result builders em todo container: Div { ... } com if, for e map dentro, sem precisar do children: [...].
  • **CLI winged**: winged new, winged build e winged 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:

  1. Type Safety: Erros de sintaxe são capturados em tempo de compilação
  2. Familiaridade: Se você já conhece Swift, vai se sentir em casa
  3. Composição: Código reutilizável e modular
  4. 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ção

Ainda 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.sh

Passo 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 config

E 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 8000

Ou, sem depender do Python, com o servidor embutido na CLI da 2.0:

winged serve --output output --no-build

O --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?

  1. Controle Total: Todo o código está no seu controle, sem dependências externas complexas
  2. Type Safety: Erros são detectados em tempo de compilação
  3. Performance: HTML estático é extremamente rápido
  4. Flexibilidade: Fácil de integrar com CI/CD e deploy em qualquer serviço de hospedagem estática
  5. 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

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.