run static method

Future<void> run({
  1. required ServerOptions options,
  2. required SeoConfig seo,
  3. required List<FlenxRoute> routes,
  4. required Component notFound,
  5. String? blog,
  6. Blog? blogInstance,
  7. List<BlogSource> blogSources = const [],
  8. bool blogFromDb = false,
  9. String blogTable = 'blog_posts',
  10. BlogLayoutBuilder? blogLayout,
  11. List<IRouteSource> extraSources = const [],
  12. List<ApiEndpoint> apis = const [],
  13. DbExecutor? db,
  14. EmailSender? onEmail,
  15. TokenVerifier? tokenVerifier,
  16. List<FlenxStyle> globalStyles = const [],
  17. List<String> rawGlobalStyles = const [],
  18. AdsConfig? ads,
  19. FlenxTracking? tracking,
  20. FlenxConsent? consent,
  21. String lang = 'pt-BR',
  22. int? port,
  23. String? primaryColor,
  24. String? primaryColorDark,
  25. String? faviconUrl,
  26. String? appleTouchIconUrl,
  27. List<String> globalScripts = const [],
  28. List<Component> floatingButtons = const [],
  29. List<String> preloadImages = const [],
  30. bool emitHtaccess = true,
})

Sobe o site com uma única chamada. routes é a fonte única: cada FlenxRoute traz o SEO e o componente. Se blog for informado, /blog* é resolvido sozinho. extraSources adiciona fontes dinâmicas de sitemap.

Implementation

static Future<void> run({
  required ServerOptions options,
  required SeoConfig seo,
  required List<FlenxRoute> routes,
  required Component notFound,
  String? blog,
  Blog? blogInstance,
  List<BlogSource> blogSources = const [],
  bool blogFromDb = false,
  String blogTable = 'blog_posts',
  BlogLayoutBuilder? blogLayout,
  List<IRouteSource> extraSources = const [],
  List<ApiEndpoint> apis = const [],
  DbExecutor? db,
  EmailSender? onEmail,
  TokenVerifier? tokenVerifier,
  // Estilos globais no padrão Flutter — lista de [FlenxStyle] (seletor +
  // props tipadas: Color, EdgeInsets, FontWeight…). Ex.:
  // `[FlenxStyle('h1', color: Colors.white, fontSize: 48)]`.
  List<FlenxStyle> globalStyles = const [],
  // Escape hatch de CSS puro: lista de strings (pode ser o conteúdo de um
  // arquivo .css). Ex.: `['.x{height:48px}', await File('site.css').readAsString()]`.
  List<String> rawGlobalStyles = const [],
  AdsConfig? ads,
  // Rastreamento/omnipresença: pixels e analytics (Ga4, MetaPixel, GoogleAds,
  // BingUet, LinkedInInsight, FirebasePush, CustomPixel). Injeta os snippets
  // no <head> + o dispatcher `flenx.track`. Nada é fixo — só o que passar.
  FlenxTracking? tracking,
  // Banner de consentimento (LGPD). Quando presente, o rastreamento espera o
  // aceite (Consent Mode negado por padrão) antes de ativar os pixels.
  FlenxConsent? consent,
  String lang = 'pt-BR',
  int? port,
  // Cor da marca: define o token CSS `--primary` (e `--primary-dark`) usado
  // por botões, links e destaques. Sem precisar escrever CSS.
  String? primaryColor,
  String? primaryColorDark,
  // Favicon: emite <link rel="icon"> e <link rel="apple-touch-icon"> no <head>.
  String? faviconUrl,
  String? appleTouchIconUrl,
  // Scripts globais (URLs) injetados em toda página com `defer`
  // (ex.: analytics: `['/assets/js/gtag.js']`).
  List<String> globalScripts = const [],
  // Botões flutuantes globais (aparecem em todas as páginas) — ex.:
  // `[FlenxFloatingButton.whatsapp(href: ...)]`.
  List<Component> floatingButtons = const [],
  // Imagens da dobra inicial (LCP) — preload com fetchpriority=high.
  List<String> preloadImages = const [],
  // Gera `.htaccess` (Apache/LiteSpeed) no build estático. Padrão: sim.
  bool emitHtaccess = true,
}) async {
  Jaspr.initializeApp(options: options);

  final byPath = {for (final r in routes) r.meta.path: r};

  final sources = <IRouteSource>[
    StaticRouteSource([for (final r in routes) r.meta]),
    ...extraSources,
  ];
  // Monta as fontes de posts (Markdown e/ou banco). Quando há fonte de banco,
  // o blog é DINÂMICO: recarrega a cada request, então posts criados pelo
  // admin em runtime aparecem sem reiniciar o servidor.
  final blogSrcs = <BlogSource>[
    if (blog != null) MarkdownBlogSource(blog),
    ...blogSources,
    if (blogFromDb && db != null) DatabaseBlogSource(db, table: blogTable),
  ];
  final dynamicBlog =
      blogInstance == null && blogSrcs.any((s) => s is DatabaseBlogSource);

  Blog? loaded = blogInstance;
  if (loaded == null && blogSrcs.isNotEmpty && !dynamicBlog) {
    loaded = await Blog.fromSources(blogSrcs);
  }
  if (loaded != null) {
    sources.add(loaded.routeSource);
  } else if (dynamicBlog) {
    sources.add(DynamicBlogRouteSource(blogSrcs));
  }
  final registry = RouteRegistry(sources);

  bool handlesBlog(String path) =>
      path == '/blog' || path.startsWith('/blog/');

  Future<Blog?> blogFor(String path) async {
    if (!handlesBlog(path)) return null;
    if (loaded != null) return loaded;
    if (dynamicBlog) return Blog.fromSources(blogSrcs);
    return null;
  }

  await FlenxServer(
    seo: seo,
    registry: registry,
    apis: apis,
    db: db,
    onEmail: onEmail,
    tokenVerifier: tokenVerifier,
    notFound: notFound,
    // Estilos tipados (FlenxStyle) convertidos em regras CSS.
    styleRules: [for (final s in globalStyles) s.toRule()],
    // CSS puro: tokens da paleta central (fonte única) + override da marca
    // (--primary) + strings do app. Componentes usam `var(--token)`; qualquer
    // projeto re-tematiza sobrescrevendo estes tokens.
    rawStyles: [
      FlenxPalette.rootCss,
      if (primaryColor != null)
        ':root{--primary:$primaryColor'
            '${primaryColorDark != null ? ';--primary-dark:$primaryColorDark;--primary-d:$primaryColorDark' : ''}}',
      ...rawGlobalStyles,
    ],
    headExtra: [
      // Rastreamento primeiro: o Consent Mode precisa vir antes do gtag.
      if (tracking != null)
        ...tracking.headComponents(requireConsent: consent != null),
      if (faviconUrl != null) link(rel: 'icon', href: faviconUrl),
      if (appleTouchIconUrl != null)
        link(rel: 'apple-touch-icon', href: appleTouchIconUrl),
      for (final src in globalScripts) script(src: src, defer: true),
      if (ads != null && ads.enabled)
        script(
          src: ads.loaderUrl,
          async: true,
          attributes: const {'crossorigin': 'anonymous'},
        ),
    ],
    floatingButtons: [
      ...floatingButtons,
      if (consent != null) consent,
    ],
    preloadImages: preloadImages,
    emitHtaccess: emitHtaccess,
    lang: lang,
    resolvePage: (path, query) async {
      final blogNow = await blogFor(path);
      if (blogNow != null && blogNow.handles(path)) {
        final page = blogNow.pageFor(
          path,
          query: query['q'],
          page: int.tryParse(query['page'] ?? '') ?? 1,
        );
        if (page == null) return null;
        return PageResult(blogLayout != null ? blogLayout(page) : page);
      }
      final route = byPath[path];
      if (route == null) return null;
      return PageResult(
        route.builder(RouteContext(path, query)),
        island: route.island,
      );
    },
  ).start(port: port);
}