RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Articles/AI Models
AI Models/2026-04-26Advanced

Use Apple FoundationModels in Rork — A Practical Guide to On-Device LLM Apps That Keep Working Offline

Call Apple's FoundationModels framework from a Rork-generated SwiftUI app to add AI features that keep working offline. Covers text generation, streaming, structured output via @Generable, and tool calling — all with full Swift code and production-ready error handling.

Rork515Apple Intelligence3FoundationModelsOn-Device LLMiOS 264Swift48AI30

The month my OpenAI bill crossed three hundred dollars, I finally accepted that my indie revenue model was broken. The more daily users I gained, the more API spend I burned, and I couldn't raise prices because two competitors were giving the same feature away for free. Every successful day on the App Store was making my margins worse, not better. If you're an indie developer shipping AI-powered features, you've probably felt the same squeeze: the success metric and the cost metric move together, and there's no obvious way out without compromising either price or product quality.

Then Apple shipped FoundationModels in iOS 26. Suddenly I could call a roughly three-billion-parameter language model running entirely on the user's iPhone, in a few lines of Swift, with zero per-token cost and zero network latency. No more guessing whether a viral spike would blow my Stripe ledger up. No more rate-limit headaches. The trade-offs are real and we'll get to them, but the basic equation changed for the kinds of features I want to build.

This guide walks through how I integrated FoundationModels into two of my Rork-generated SwiftUI apps. We'll start with a minimal text-generation example, then move through the three patterns I actually use in production: streaming responses, structured output with @Generable, and tool calling. Every code block is meant to be paste-and-run, and I've included the failure modes I hit so you don't have to discover them on your users.

What FoundationModels Actually Changes for App Developers

FoundationModels exposes the on-device language model that already ships with Apple Intelligence-capable iPhones, iPads, and Macs. Until iOS 26, only Apple's own consumer features — Writing Tools, Genmoji, Image Playground — could touch this model. Now developers get the raw API, and that's the difference that matters: you choose the prompt, the system instruction, the schema, the tools, and the integration points. The model itself is the same one Apple uses internally, which means it's already been validated against Apple's own quality and safety bars.

The practical difference from cloud LLMs comes down to three numbers that should reshape how you design features. Cost: zero per token, since inference happens on the user's hardware. Whatever battery and silicon they paid for is what you're using. Latency: no round trip, so first-token times beat almost any cloud provider on Wi-Fi and crush them on cellular. Privacy: user data never leaves the device, which simplifies your privacy policy, your App Store nutrition labels, and the conversations you have with enterprise customers about data residency.

That said, FoundationModels is not a drop-in replacement for GPT-5 or Claude Opus. After running both side by side for several weeks, here's how I think about the trade-offs:

  • Capability ceiling: For complex multi-step reasoning, very long-form writing, or specialised domains like legal analysis or medical synthesis, the cloud frontier models still pull ahead. FoundationModels shines at short, focused tasks — summarising, classifying, extracting fields, rewriting in a known style, generating UI copy. If your feature is "given this paragraph, produce a title," it's perfect. If your feature is "draft a legal contract from these bullet points," you probably want a frontier model
  • Device requirements: Apple Intelligence-capable hardware only. That's iPhone 15 Pro and newer, A17 Pro / M1 iPads and newer, and M-series Macs. On older devices you need a fallback path — either disable the feature, swap to a cloud model, or use a smaller specialised on-device model like a Core ML classifier
  • Context window: About 4,096 tokens, considerably less than the 100k+ available from cloud models. RAG over long documents, multi-turn conversations with deep history, and big system prompts all need engineering. The good news is that for the focused tasks where FoundationModels is strongest, 4K is usually enough

The way I frame it now: FoundationModels isn't a replacement for cloud LLMs, it's a partner that lets me stop billing my users for the things that don't actually need a frontier model. Many features in my apps used to call GPT-4 for tasks that, in retrospect, were embarrassingly simple — extracting a city name, classifying a tone, summarising a paragraph. Those are exactly the cases where FoundationModels wins on cost, latency, and privacy without sacrificing perceived quality. If you also work with custom Core ML models, the Core ML on-device guide for Rork pairs naturally with this article and is worth keeping open in another tab.

Setting Up a Rork Project to Use FoundationModels

The setup is shorter than I expected. After Rork generates your SwiftUI project, you don't need to add capabilities, frameworks, or Info.plist entries. Just import FoundationModels and you're in. The first time I tried it I spent half an hour looking for a missing setting that didn't exist, because every previous Apple SDK I'd worked with required some kind of declaration. FoundationModels is unusually frictionless on this front.

The one piece you absolutely cannot skip is the availability check. Older devices, devices with Apple Intelligence turned off, and devices still downloading the model file will all fail at runtime if you don't handle them. Treat the availability check as a launch-time concern, not an afterthought you add later. Here's the helper I now drop into every project on day one:

// FoundationModelsAvailability.swift
// Call at app launch or feature entry to decide which UI to show.
import FoundationModels
 
enum LLMAvailability {
    case ready
    case appleIntelligenceOff   // user disabled in Settings
    case downloading            // model still being downloaded
    case unsupportedDevice      // hardware not eligible
    case unknown(String)
}
 
func checkLLMAvailability() -> LLMAvailability {
    let model = SystemLanguageModel.default
    switch model.availability {
    case .available:
        return .ready
    case .unavailable(let reason):
        switch reason {
        case .appleIntelligenceNotEnabled:
            return .appleIntelligenceOff
        case .modelNotReady:
            return .downloading
        case .deviceNotEligible:
            return .unsupportedDevice
        @unknown default:
            return .unknown(String(describing: reason))
        }
    }
}
 
// Expected behaviour:
// iPhone 15 Pro on iOS 26+ with Apple Intelligence ON  → .ready
// iPhone 13 (any iOS)                                  → .unsupportedDevice
// Apple Intelligence toggled off in Settings           → .appleIntelligenceOff

The @unknown default matters more than it looks. Apple has signalled that more unavailable reasons are coming, and exhaustive enum matches that compile today will silently drop new cases later. I treat compiler-side exhaustiveness as a useful hint, not a substitute for actual fallback behaviour at runtime. Whenever a new iOS minor release lands, give your availability check a quick re-test on the device matrix you support.

A Minimum Text Generation View in 30 Lines

Here's the smallest end-to-end view I'd ship to TestFlight. My very first version was five lines shorter — I cut error handling — and I regretted it within a day, so this one keeps it from the start. The temptation when prototyping with a new API is to skip the error path until "things work," but with LLM APIs the error path is where most of your real-world traffic ends up living, so build it in from line one.

// SimpleAskView.swift — minimal request/response sample
import SwiftUI
import FoundationModels
 
struct SimpleAskView: View {
    @State private var prompt = ""
    @State private var answer = ""
    @State private var isLoading = false
    @State private var errorMessage: String?
 
    var body: some View {
        VStack(spacing: 16) {
            TextField("Ask anything", text: $prompt)
                .textFieldStyle(.roundedBorder)
            Button("Send") { Task { await ask() } }
                .disabled(prompt.isEmpty || isLoading)
            if let errorMessage { Text(errorMessage).foregroundStyle(.red) }
            ScrollView { Text(answer).padding() }
        }
        .padding()
    }
 
    @MainActor
    private func ask() async {
        isLoading = true
        defer { isLoading = false }
        errorMessage = nil
        do {
            let session = LanguageModelSession(
                instructions: "Answer concisely in one short paragraph."
            )
            let response = try await session.respond(to: prompt)
            answer = response.content
        } catch {
            errorMessage = "Request failed: \(error.localizedDescription)"
        }
    }
}
 
// Example output:
// Prompt: "What is Rork?"
// Answer: "Rork is an AI tool that turns natural-language descriptions into mobile apps..."

Two notes on the choices here. First, the instructions parameter is your system prompt. Without it I sometimes saw responses drift into a different language, change tone mid-conversation, or pad out short answers with unnecessary preambles. With a short, specific instruction, behaviour becomes far more predictable. Second, marking ask() @MainActor keeps SwiftUI happy without scattering await MainActor.run { } blocks around UI updates, and it keeps the threading story easy to reason about when you come back to the code six months later.

A small detail that matters in production: notice that session is created fresh inside ask(). For a one-shot question this is fine. If you're building a chat experience with continuity, you'll want to keep the same LanguageModelSession across turns so the model has access to prior messages within its context window. We'll see that pattern in the streaming section below.

Streaming Makes the App Feel 3× Faster

respond(to:) blocks until the full response is ready. For a one-sentence answer that's fine, but for explanations or summaries, users feel the wait. I switched one of my apps to streaming and bounce rate on that screen dropped roughly 30%. Nothing about inference got faster — it's purely a perceived-speed effect, and it's worth chasing because it costs you almost no extra code.

// StreamingAskView.swift — render chunks as they arrive
import SwiftUI
import FoundationModels
 
@MainActor
final class StreamingAskViewModel: ObservableObject {
    @Published var partial = ""
    @Published var isStreaming = false
    @Published var errorMessage: String?
 
    private let session = LanguageModelSession(
        instructions: "Reply in three short paragraphs at most."
    )
 
    func ask(prompt: String) async {
        partial = ""
        isStreaming = true
        errorMessage = nil
        defer { isStreaming = false }
        do {
            let stream = session.streamResponse(to: prompt)
            for try await snapshot in stream {
                // snapshot.content is the cumulative text. Replace, don't append.
                partial = snapshot.content
            }
        } catch {
            errorMessage = "Streaming failed: \(error.localizedDescription)"
        }
    }
}
 
// Expected behaviour:
// User taps "Tell me about Rork's strengths." Text appears character by character.
// First-token-on-screen lands around 1.2 s on A17 Pro in my measurements.

The for try await snapshot in stream loop gives you snapshot.content containing the cumulative string so far. Replace it on every iteration; do not try to compute deltas yourself. I burned an hour on a delta-stitching bug that produced doubled punctuation, and the fix was simply to trust Apple's accumulator and overwrite each frame. The framework is doing the right thing here — your job is to render, not to reconstruct.

I keep the @MainActor annotation on the whole view model so SwiftUI can observe @Published directly. Mixing actors here is where most threading warnings come from in my experience, and the warnings are easy to ignore until they bite you with a concurrency-related crash that only reproduces on real hardware. If you find yourself reaching for Task { @MainActor in ... } repeatedly, consider hoisting the actor annotation to the type level instead — it usually means you're racing UI updates against background work, and the simplest fix is to keep the view model on the main actor and offload only specific heavy work to background tasks via await Task.detached.

One more streaming pattern worth knowing: cancellation. If the user navigates away mid-stream, the Task that started the stream should be cancelled. SwiftUI gives you .task and .task(id:) modifiers that handle this for free — when the view leaves the hierarchy, the task is cancelled. Inside the loop, Task.checkCancellation() lets you bail early if you're doing post-processing on each chunk. Skipping cancellation is the most common cause of "why is my app draining battery in the background" reports.

Killing JSON Parsing With @Generable

Plenty of real product features need structured output: extracting tasks from free text, pulling colour and category from an image description, turning a voice note into a calendar entry. With cloud LLMs the workflow is "ask for JSON, then JSONDecoder, then handle the inevitable malformed output." With FoundationModels, the @Generable macro removes that whole layer. The model is constrained at the API level to produce values that match your Swift types, and you skip the parse-and-validate dance entirely.

// TaskExtractor.swift — turn free-form notes into typed TODO items
import FoundationModels
 
@Generable
struct ExtractedTask {
    @Guide(description: "Short task name (max 40 chars)")
    let title: String
    @Guide(description: "Priority: must be one of high / medium / low")
    let priority: String
    @Guide(description: "Estimated minutes; nil if unknown")
    let durationMinutes: Int?
}
 
@Generable
struct ExtractionResult {
    let tasks: [ExtractedTask]
}
 
func extractTasks(from memo: String) async throws -> [ExtractedTask] {
    let session = LanguageModelSession(
        instructions: "Extract tasks from the user's note. Reply in English."
    )
    let result = try await session.respond(
        to: memo,
        generating: ExtractionResult.self
    )
    return result.content.tasks
}
 
// Expected behaviour:
// Input: "Finish slides by tomorrow. Buy groceries. Dentist next week."
// Output:
//   ExtractedTask(title: "Finish slides", priority: "high", durationMinutes: 90)
//   ExtractedTask(title: "Buy groceries", priority: "medium", durationMinutes: nil)
//   ExtractedTask(title: "Book dentist", priority: "low", durationMinutes: nil)

@Guide is where you steer accuracy. Saying "must be one of high / medium / low" cut my rate of stray values like "urgent" or "asap" to roughly zero. Pair the type system with natural-language hints — that combination is what makes @Generable worth using over hand-rolled JSON parsing. Think of @Guide as a place to encode constraints that aren't expressible in the type system itself. Enums would be even tighter, and the framework supports them, but I find the string-with-guide pattern is more forgiving when you want to add a new category later without forcing a migration.

You can pipe the array straight into SwiftData or Realm and you're 80% of the way to a memo-to-TODO app. The remaining 20% is UX polish: showing the extracted items in a confirm screen before committing, letting the user edit them, and handling the case where the model returns zero tasks ("the model thought your note had no actionable items — is that right?"). If you're building the SwiftUI side from scratch with Rork, the Rork SwiftUI native iOS development guide covers the surrounding architecture for this kind of feature.

A subtle but important point about @Generable types: they should be plain value types with no business logic. Resist the urge to add computed properties, validation methods, or persistence hooks. The macro generates supporting code based on the literal stored properties, and complex types make the generated code harder to debug when something goes wrong. Keep the generated types as data carriers, then convert them to your richer domain types in a separate step.

Tool Calling — Letting the Model Reach Into Your App

Sometimes you want the model to do more than reply with text — you want it to schedule a timer, look up a weather forecast, or query your local database. That's tool calling. You define types conforming to the Tool protocol, hand them to the session, and the model decides when to invoke them based on the user's prompt. Tool calling turns the LLM from a "text in, text out" function into a planner that can drive real app features.

// TimerTool.swift — a tool the model can call to schedule a local notification
import FoundationModels
 
struct TimerTool: Tool {
    let name = "scheduleTimer"
    let description = "Schedule a local notification N seconds from now."
 
    @Generable
    struct Arguments {
        @Guide(description: "Seconds from now (1 to 86400)")
        let seconds: Int
        @Guide(description: "Title to display in the notification")
        let title: String
    }
 
    @Generable
    struct Output {
        let scheduled: Bool
        let firesAt: String  // ISO 8601
    }
 
    func call(arguments: Arguments) async throws -> Output {
        guard (1...86400).contains(arguments.seconds) else {
            throw NSError(
                domain: "TimerTool",
                code: 1,
                userInfo: [NSLocalizedDescriptionKey: "seconds out of range"]
            )
        }
        let fireDate = Date().addingTimeInterval(TimeInterval(arguments.seconds))
        await NotificationScheduler.shared.schedule(at: fireDate, title: arguments.title)
        let iso = ISO8601DateFormatter().string(from: fireDate)
        return Output(scheduled: true, firesAt: iso)
    }
}
 
// Usage
let session = LanguageModelSession(tools: [TimerTool()])
let response = try await session.respond(
    to: "Remind me in 10 minutes to check the laundry."
)
 
// Expected behaviour:
// The model internally calls TimerTool.call(.init(seconds: 600, title: "Check the laundry")),
// receives Output(scheduled: true, firesAt: "2026-04-26T20:25:00Z"),
// and returns natural language: "Got it — I'll remind you in 10 minutes about the laundry."

The principle I follow when designing tools is "validate inside the tool, even if you trust the caller." That's what the 1...86400 guard is for. Most of the time the model passes sensible values, but I have seen seconds: 0 and absurdly large numbers slip through. Throwing a clean error gives the model a chance to recover — it usually re-asks the user or tries different arguments — which means you ship a more robust assistant with very little extra code. This is one of those places where the few extra lines pay off disproportionately.

A natural follow-up question is "how many tools should I attach?" In my experience, fewer is better. The model has to reason about which tool to call, and a long menu makes that reasoning slower and more error-prone. I try to keep individual sessions limited to four or five tools at most, and I split unrelated capabilities into separate sessions when I can. If you find yourself wanting ten tools, that's usually a sign the feature should be broken into multiple narrower ones, each with its own focused tool palette.

Three Failure Modes I Hit in Production

These caught me by surprise. Knowing them up front saves real debugging time and at least one frustrated TestFlight cycle.

1. More users on unsupported devices than you expect. In one of my apps, roughly 40% of session starts returned .unavailable. Older iPhones, Apple Intelligence turned off, and the model still downloading after iOS update were all common. Hide the feature UI entirely or gracefully fall back to a cloud LLM — leaving a dead menu item in the UI also tends to trigger App Store reviewer frowns. If you have analytics, log the availability state on every launch so you can see the actual distribution in your user base instead of guessing from forum threads.

2. Context overflow with longer prompts. The published context window is around 4,096 tokens. Catching LanguageModelSession.Error.contextWindowExceeded and either trimming the prompt or summarising older context is the realistic mitigation. I keep a "retry with the first 2,000 characters only" fallback that has saved several support tickets. For chat-style features, set a hard cap on history and use the model itself to summarise older turns into a compact representation when you approach the limit. This is the same pattern cloud LLM apps use — it just kicks in earlier with FoundationModels.

3. Safety filter rejections on legitimate prompts. Medical, legal, and graphic content gets refused. Fair enough — but a "help me brainstorm a thriller plot" prompt can also get caught. When response.content comes back empty or boilerplate-refusal, surface a "try rephrasing" hint and consider offering a switch to a cloud model for users who actually need it. I've seen creative writing apps pick up surprisingly good user reviews simply because they handle the refusal case gracefully — telling the user "this prompt was filtered, here's how to rephrase it" is more useful than silently returning nothing.

Pairing FoundationModels With a Cloud LLM Without Doubling Complexity

The most useful pattern I've landed on isn't "either/or" — it's a small router that picks between FoundationModels and a cloud LLM based on the request. The router is twenty lines of Swift and pays for itself within a week of running.

The decision rule is simple. If the prompt is short, the task is bounded, and the device supports FoundationModels, route on-device. Otherwise route to the cloud. "Short and bounded" in practice means under about 1,500 input characters, single-turn, and one of a known set of feature types — extract, classify, summarise, rewrite. Anything that asks for long-form generation, deep reasoning, or up-to-date world knowledge goes to the cloud.

// LLMRouter.swift — pick on-device or cloud based on request shape
import FoundationModels
 
enum LLMRoute { case onDevice, cloud }
 
struct RouterRequest {
    let kind: Kind            // extract / classify / summarise / freeform
    let inputCharacters: Int
    let needsFreshness: Bool  // true for "what is the latest..." queries
    enum Kind { case extract, classify, summarise, rewrite, freeform }
}
 
func chooseRoute(for request: RouterRequest) -> LLMRoute {
    if request.needsFreshness { return .cloud }
    if request.kind == .freeform { return .cloud }
    if request.inputCharacters > 1500 { return .cloud }
    if checkLLMAvailability() != .ready { return .cloud }
    return .onDevice
}
 
// Expected behaviour:
// "Extract tasks from this 500-char note" → .onDevice
// "Write me a 1000-word blog post"        → .cloud
// "What did Apple announce yesterday?"    → .cloud (needsFreshness)
// On an iPhone 13 (no FoundationModels)   → .cloud (availability)

The win here is operational. Once the router exists, you can move features between routes without touching their UI. When Apple ships a larger model, you push the boundary upward; when your cloud bill spikes, you push it downward. You also get a natural place to measure: log the route on every request, and you can see exactly which workloads are on-device versus cloud, and what the cost split looks like.

One pitfall to avoid: don't design two completely separate prompt strategies for the two routes. The cleanest approach is a single prompt template that both backends accept, with the router only choosing where to send it. That keeps your prompt engineering effort consolidated and means switching routes doesn't require re-tuning quality.

Pre-Release Checklist

This is the list I run through before every TestFlight build that touches FoundationModels. Working from it has saved me at least one App Store rejection and several embarrassing post-launch hotfixes.

  • Are you calling SystemLanguageModel.default.availability at launch and hiding feature UI on .unavailable?
  • When the user navigates away mid-stream, is the Task cancelled so inference doesn't keep running and draining battery?
  • Do all @Generable fields have a @Guide annotation to steer accuracy?
  • Are tool arguments validated inside the tool, not just trusted from the model?
  • Is there at least one path where errorMessage reaches the UI? Silent failures destroy trust faster than visible ones
  • Have you tested with Apple Intelligence toggled off, on TestFlight, at least once?
  • Does your App Store review note mention that you use FoundationModels and that user data stays on device? Reviewers reward this clarity
  • Are you running an A/B test comparing the FoundationModels path to your existing cloud-LLM path on retention and answer quality?
  • Have you measured actual first-token-to-screen latency on the cheapest supported device, not just your dev iPhone?
  • Is there an analytics event firing whenever the model returns a refusal, so you can monitor false-positive filtering over time?

If you're also touching iOS 26 visuals, the Rork iOS 26 Liquid Glass practical guide pairs naturally with this article — together they cover both the intelligence and the look-and-feel of an iOS 26 app, which is what reviewers and users notice first.

Closing — Replace One Feature First, Not the Whole App

New APIs are tempting to adopt all at once, but full rewrites make rollback and measurement painful. The approach that worked for me is to pick a single feature in an existing app — preferably one that returns short, focused answers — and replace its cloud call with FoundationModels. Ship it to TestFlight, run it for a week, and watch three numbers: bounce rate on that screen, crash rate, and qualitative feedback from your beta testers.

Whether FoundationModels frees you from your AI bill or quietly drives users away depends on your specific use case, and that answer doesn't exist on paper. It exists in TestFlight. Pick the feature tonight, swap it out tomorrow, and let real usage decide.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

AI Models2026-07-05
When Your Finance App's AI Keeps Dumping Everything Into 'Other' — Field Notes on Catching Silent Classification Drift
An AI expense classifier in a Rork finance app can be accurate at launch and quietly decay over months until the monthly advice goes wrong. Here is how I instrumented confidence and category distribution to get ahead of the drift, with working code.
AI Models2026-06-17
Where to Stop Letting Rork Fix Your Bugs: A Triage Routine for the 30% That Need You
Most bugs you hand Rork get fixed in a couple of regenerations. A stubborn minority loop forever, each fix spawning a new symptom. Here is the triage routine I use to split what to delegate from what to take over by hand, with retreat lines, regression guards, and a decision log.
AI Models2026-05-05
A Prompt Design Guide for Getting Production-Ready UI from Rork's AI
Learn how Rork's AI interprets prompts and how to craft them so that forms, lists, and cards come out the way you actually intended — with less manual cleanup afterward.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →