17: SEO

The seo resource centralises the search and sharing metadata an app needs: sitemap.xml, robots.txt, hreflang alternates, the canonical link, and a <head> block of <meta> description plus Open Graph and Twitter cards. It derives everything from props you already set, so a uniform, complete-but-restrained set ships from one place.

17.1: Activation

Add the resource (it needs output):

{
    "resources": [..., "seo", "output"]
}

That alone serves two routes:

Route Output
GET /sitemap.xml A multilingual sitemap built from %app->pages and %app->langs
GET /robots.txt Allow or Disallow, gated on the indexable constant (see X.4)

The <head> metadata is opt-in per page: render %seo->head where you want it (X.3).

17.2: The sitemap and hreflang

The sitemap iterates %app->pages and, for each page, emits an hreflang alternate per entry in %app->langs plus an x-default. Localised paths come from %app->slugs (a uri => localised-uri map) when you have them; otherwise the path is prefixed with the language code.

In your <head>, the same alternates belong as <link rel=alternate hreflang> tags. The resource gives you one view for that, so you loop your languages and let seo format each link:

view head:
<foreach array_keys(%app->langs) AS $lang>
    {{ %seo->link($lang, $lang === %app->lang ? %req->uri : "/$lang".%req->uri) }}
</foreach>
{{ %seo->head }}

The app owns which URLs are alternates (it knows its own routing and localisation); seo owns the markup.

17.3: The head block

%seo->head renders the conventional metadata set, all derived from existing props:

It is composable, not a replacement: keep your app's own head tags (title, CSRF, styles) and add %seo->head alongside them. There are no keywords and no marketing filler; the set is Open Graph, Twitter card, canonical, hreflang, description and robots, which is the usual complete-but-restrained baseline.

The defaults read from props you already have:

Property Default source
og:title The document title
og:description / description %app->description
og:image %app->image, falling back to /icon.webp at the site root
og:url / canonical The current request URL
og:site_name The app id
og:locale Derived from %app->lang (for example nl becomes nl_NL)
og:type website

17.4: robots.txt and the indexable constant

robots.txt is generated, and it is safe by default. Unless the app sets the indexable constant to a truthy value, the resource serves:

User-agent: *
Disallow: /

So dev, stage and any non-public host are de-indexed simply by not declaring indexable. Set it only on the real production entrypoint, where you also set the real host:

phlo_app(
    id: 'Example',
    host: 'example.com',
    indexable: true,
);

With indexable: true the resource serves an Allow: /, one Disallow: line per entry in %app->robotsDisallow, and a Sitemap: line. Because robots.txt comes from the route, there is no static robots.txt to maintain or to accidentally deploy from a dev host.

17.5: Per-app and per-page overrides

The defaults cover most apps. Override the rest with the cross-class injection idiom (see the Advanced chapter), which sets the prop at build time:

prop %seo.twitterCard = true        // opt in to the Twitter summary card
prop %seo.ogType = 'article'        // override the Open Graph type
prop %seo.siteName = 'Example Co'   // override the site name (default: the app id)

Two opt-outs are read per request from app props, so a single page can drop out of the index without touching the rest of the site:

Property Effect
%app->image The default Open Graph image for the whole app
%app->noindex (or %app->noLink) Marks the current page noindex,follow and drops its canonical link

Set %app->noindex in a route before rendering to keep that one page out of search results while the rest of the app stays indexable.

我们使用必要的cookie来使该网站正常工作。在您的许可下,我们还使用分析工具来改善网站。