@maccesar/aiskills 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +42 -3
  2. package/commands/release.md +2 -4
  3. package/lib/commands/list.js +109 -22
  4. package/lib/commands/skills.js +1 -3
  5. package/lib/config.js +1 -0
  6. package/lib/prompts/checkboxCancel.js +0 -6
  7. package/lib/symlink.js +1 -1
  8. package/package.json +4 -2
  9. package/skills/refactoring-ui/SKILL.md +3 -7
  10. package/skills/seo-launch/SKILL.md +91 -0
  11. package/skills/seo-launch/assets/head.php +85 -0
  12. package/skills/seo-launch/assets/htaccess-static +86 -0
  13. package/skills/seo-launch/assets/robots.txt +17 -0
  14. package/skills/seo-launch/assets/social-meta.blade.php +90 -0
  15. package/skills/seo-launch/references/head-tags.md +109 -0
  16. package/skills/seo-launch/references/images.md +86 -0
  17. package/skills/seo-launch/references/search-engines.md +77 -0
  18. package/skills/seo-launch/references/server-files.md +168 -0
  19. package/skills/seo-launch/references/structured-data.md +139 -0
  20. package/skills/seo-launch/scripts/__pycache__/auditar_seo.cpython-312.pyc +0 -0
  21. package/skills/seo-launch/scripts/auditar_seo.py +539 -0
  22. package/skills/session-log/SKILL.md +83 -311
  23. package/skills/session-log/evals/README.md +16 -52
  24. package/skills/session-log/evals/ab-ronda-1.md +22 -67
  25. package/skills/session-log/evals/ab-ronda-2.md +28 -113
  26. package/skills/session-log/evals/defecto-experimento.md +3 -8
  27. package/skills/session-log/references/file-layout.md +44 -142
  28. package/skills/session-log/references/verification.md +22 -56
  29. package/skills/stitch-showcase/references/12-video-embedding.md +11 -28
  30. package/skills/stitch-showcase/references/13-language-detection.md +13 -38
  31. package/skills/stitch-showcase/references/14-troubleshooting-known-issues.md +18 -47
  32. package/skills/vscode-extension-dev/SKILL.md +3 -7
  33. package/skills/stitch-showcase/scripts/__pycache__/build_showcase.cpython-314.pyc +0 -0
  34. package/skills/stitch-showcase/scripts/__pycache__/component_utils.cpython-313.pyc +0 -0
  35. package/skills/stitch-showcase/scripts/__pycache__/component_utils.cpython-314.pyc +0 -0
  36. package/skills/stitch-showcase/scripts/__pycache__/detect_components.cpython-313.pyc +0 -0
  37. package/skills/stitch-showcase/scripts/__pycache__/detect_components.cpython-314.pyc +0 -0
  38. package/skills/stitch-showcase/scripts/__pycache__/extract_catalog.cpython-313.pyc +0 -0
  39. package/skills/stitch-showcase/scripts/__pycache__/extract_catalog.cpython-314.pyc +0 -0
  40. package/skills/stitch-showcase/scripts/__pycache__/extract_text.cpython-313.pyc +0 -0
  41. package/skills/stitch-showcase/scripts/__pycache__/extract_text.cpython-314.pyc +0 -0
  42. package/skills/stitch-showcase/scripts/__pycache__/extract_zips.cpython-313.pyc +0 -0
  43. package/skills/stitch-showcase/scripts/__pycache__/extract_zips.cpython-314.pyc +0 -0
  44. package/skills/stitch-showcase/scripts/__pycache__/parse_design_md.cpython-313.pyc +0 -0
  45. package/skills/stitch-showcase/scripts/__pycache__/parse_design_md.cpython-314.pyc +0 -0
  46. package/skills/stitch-showcase/scripts/__pycache__/slug_demangle.cpython-314.pyc +0 -0
@@ -0,0 +1,90 @@
1
+ {{--
2
+ Every tag a page needs to be indexed and to render a card when its link is shared.
3
+
4
+ Put it in the layout's <head>, after the charset and viewport:
5
+
6
+ <x-social-meta
7
+ title="Freight and warehousing in Reynosa | Acme"
8
+ description="…"
9
+ :image="$nota?->imagen_url"
10
+ type="article" />
11
+
12
+ Per-page values arrive as props; the site-wide ones come from config. The
13
+ fallbacks are what make it safe to drop into a layout: a page that passes
14
+ nothing still emits a complete, valid block.
15
+
16
+ Add to config/app.php (or a dedicated config file):
17
+
18
+ 'site_name' => env('APP_NAME'),
19
+ 'og_image' => env('APP_URL') . '/images/og-image.jpg',
20
+ 'og_locale' => 'es_MX',
21
+ 'theme_color' => '#0f2d52',
22
+ --}}
23
+
24
+ @props([
25
+ 'title' => null,
26
+ 'description' => null,
27
+ 'url' => null,
28
+ 'image' => null,
29
+ 'imageAlt' => null,
30
+ 'type' => 'website',
31
+ 'locale' => null,
32
+ 'robots' => 'index, follow',
33
+ 'publishedAt' => null,
34
+ 'modifiedAt' => null,
35
+ ])
36
+
37
+ @php
38
+ $siteName = config('app.site_name', config('app.name'));
39
+ // url()->current() drops the query string, which is what a canonical wants:
40
+ // ?page=2&utm_source=… would otherwise mint a distinct URL per visit.
41
+ $canonical = $url ?? url()->current();
42
+ $metaTitle = $title ?? $siteName;
43
+ $metaDescription = $description ?? config('app.description', '');
44
+ // Absolute URL on purpose: Facebook, WhatsApp and X discard a relative
45
+ // image path — no error, no thumbnail.
46
+ $metaImage = $image ? (str_starts_with($image, 'http') ? $image : url($image)) : config('app.og_image');
47
+ $metaImageAlt = $imageAlt ?? $metaTitle;
48
+ @endphp
49
+
50
+ <title>{{ $metaTitle }}</title>
51
+ <meta name="description" content="{{ $metaDescription }}">
52
+ <link rel="canonical" href="{{ $canonical }}">
53
+ <meta name="robots" content="{{ $robots }}">
54
+ <meta name="theme-color" content="{{ config('app.theme_color', '#0f2d52') }}">
55
+
56
+ {{-- An SVG favicon has no intrinsic size and draws sharp at every size iOS,
57
+ Safari and Chrome ask for. iOS still needs the PNG for the home screen. --}}
58
+ <link rel="icon" type="image/svg+xml" href="{{ asset('images/logo-symbol.svg') }}">
59
+ <link rel="apple-touch-icon" sizes="180x180" href="{{ asset('images/apple-touch-icon.png') }}">
60
+ <meta name="apple-mobile-web-app-title" content="{{ $siteName }}">
61
+
62
+ {{-- Open Graph: Facebook, WhatsApp, LinkedIn --}}
63
+ <meta property="og:type" content="{{ $type }}">
64
+ <meta property="og:locale" content="{{ $locale ?? config('app.og_locale', 'es_MX') }}">
65
+ <meta property="og:site_name" content="{{ $siteName }}">
66
+ <meta property="og:url" content="{{ $canonical }}">
67
+ <meta property="og:title" content="{{ $metaTitle }}">
68
+ <meta property="og:description" content="{{ $metaDescription }}">
69
+ <meta property="og:image" content="{{ $metaImage }}">
70
+ <meta property="og:image:secure_url" content="{{ $metaImage }}">
71
+ <meta property="og:image:type" content="image/jpeg">
72
+ {{-- These must match the real file: they exist so the client can lay out the
73
+ card before downloading it. --}}
74
+ <meta property="og:image:width" content="1200">
75
+ <meta property="og:image:height" content="630">
76
+ <meta property="og:image:alt" content="{{ $metaImageAlt }}">
77
+
78
+ @if ($type === 'article' && $publishedAt)
79
+ <meta property="article:published_time" content="{{ $publishedAt->toIso8601String() }}">
80
+ @if ($modifiedAt)
81
+ <meta property="article:modified_time" content="{{ $modifiedAt->toIso8601String() }}">
82
+ @endif
83
+ @endif
84
+
85
+ {{-- X. summary_large_image is the one that shows the photo full width. --}}
86
+ <meta name="twitter:card" content="summary_large_image">
87
+ <meta name="twitter:title" content="{{ $metaTitle }}">
88
+ <meta name="twitter:description" content="{{ $metaDescription }}">
89
+ <meta name="twitter:image" content="{{ $metaImage }}">
90
+ <meta name="twitter:image:alt" content="{{ $metaImageAlt }}">
@@ -0,0 +1,109 @@
1
+ # The `<head>` tags
2
+
3
+ Everything that goes in the head, what each tag does, and the mistakes that produce a tag that is present and useless.
4
+
5
+ ---
6
+
7
+ ## Basics
8
+
9
+ ```html
10
+ <meta charset="utf-8">
11
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
12
+ <title>Freight and warehousing in Reynosa | Acme Logistics</title>
13
+ <meta name="description" content="Freight, storage and crossdock from any state to the northeast border. Quotes the same day.">
14
+ ```
15
+
16
+ **`<title>` — the searched words go first, under ~60 characters.** Google truncates around 60 and cuts the tail, which is where the brand usually sits. One title per page: repeating the same one across the site makes every page compete for the same query. The pattern that survives truncation is `Specific thing | Brand`.
17
+
18
+ **`description` — 120 to 160 characters.** It does not influence ranking; it is the text the user reads in the results and it is what decides the click. Under 70 characters wastes the space; past 165 it gets cut. Write it per page. When it is auto-generated from a template, check what it produces: a description built from the site name alone says nothing about the page.
19
+
20
+ **`keywords` — Google stopped using it in 2009**, and Bing does not read it either. Not harmful; just do not let anyone believe it is doing work. If the client asks for it, add it and say what it is worth.
21
+
22
+ ## Canonical
23
+
24
+ ```html
25
+ <link rel="canonical" href="https://example.com/">
26
+ ```
27
+
28
+ Tells search engines which address is the official one when several serve the same content. **Absolute URL, always** — a relative canonical is either ignored or resolved against the wrong base.
29
+
30
+ It is a hint. The 301 redirects in the server config (`references/server-files.md`) are the enforcement. Ship both: the canonical for the crawler that follows hints, the 301 for everything else.
31
+
32
+ The canonical of a page points at itself, not at the home page. A site-wide canonical pointing at `/` tells Google that every page is a duplicate of the home page, and they drop out of the index.
33
+
34
+ ## Robots and theme-color
35
+
36
+ ```html
37
+ <meta name="robots" content="index, follow">
38
+ <meta name="theme-color" content="#0f2d52">
39
+ ```
40
+
41
+ `index, follow` is the default behaviour anyway; declaring it is a statement of intent that makes a stray `noindex` obvious in a diff. **Check production for a leftover `noindex`** — it is the single most effective way to be invisible, and it usually arrives from a staging template.
42
+
43
+ `theme-color` paints the browser bar on mobile with the site's colour. Small, and it shows.
44
+
45
+ ## Open Graph — WhatsApp, Facebook, LinkedIn
46
+
47
+ This block is what produces the card with image, title and description when someone pastes the link.
48
+
49
+ ```html
50
+ <meta property="og:type" content="website">
51
+ <meta property="og:locale" content="es_MX">
52
+ <meta property="og:site_name" content="Acme Logistics">
53
+ <meta property="og:url" content="https://example.com/">
54
+ <meta property="og:title" content="Freight, warehousing and crossdock">
55
+ <meta property="og:description" content="…">
56
+ <meta property="og:image" content="https://example.com/images/og-image.jpg">
57
+ <meta property="og:image:secure_url" content="https://example.com/images/og-image.jpg">
58
+ <meta property="og:image:type" content="image/jpeg">
59
+ <meta property="og:image:width" content="1200">
60
+ <meta property="og:image:height" content="630">
61
+ <meta property="og:image:alt" content="…">
62
+ ```
63
+
64
+ - **`og:title` is not the `<title>`.** The browser title carries the brand at the end for the search engine; the card title goes without it, because `og:site_name` already prints it right below. Repeating it wastes the width.
65
+ - **`og:type`**: `website` for the home page and static pages, `article` for a post or a news item. With `article` you can add `article:published_time` and `article:author`.
66
+ - **`width` and `height` explicit.** Without them WhatsApp sometimes shows a small thumbnail on first load while it downloads the image to measure it. They must match the real file — see the hard rules.
67
+ - **`og:image:secure_url`** is the HTTPS variant some older clients ask for. Same value; costs one line.
68
+ - **`og:image:alt`** is the alt text of the thumbnail, for screen readers.
69
+ - **`og:locale`** in the `xx_XX` form (`es_MX`, `en_US`), not the bare language code.
70
+
71
+ ## Twitter / X
72
+
73
+ ```html
74
+ <meta name="twitter:card" content="summary_large_image">
75
+ <meta name="twitter:title" content="…">
76
+ <meta name="twitter:description" content="…">
77
+ <meta name="twitter:image" content="https://example.com/images/og-image.jpg">
78
+ <meta name="twitter:image:alt" content="…">
79
+ ```
80
+
81
+ `summary_large_image` shows the photo full width. The other value, `summary`, leaves it as a small square next to the text. X falls back to the Open Graph tags when the Twitter ones are missing, so the minimum that changes anything is `twitter:card`; the rest is worth writing when the copy should differ, because X truncates earlier than Facebook.
82
+
83
+ `twitter:site` is the site's `@handle`. Skip it rather than invent it.
84
+
85
+ ## Icons
86
+
87
+ ```html
88
+ <link rel="icon" type="image/svg+xml" href="/images/logo-symbol.svg">
89
+ <link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png">
90
+ <meta name="apple-mobile-web-app-title" content="Acme">
91
+ ```
92
+
93
+ Why an SVG, and why iOS still needs the PNG: `references/images.md`.
94
+
95
+ `apple-mobile-web-app-title` is the name under the icon on the iOS home screen. Without it, iOS takes the full `<title>` and cuts it wherever it fits, which for a keyword-first title is unreadable.
96
+
97
+ ## Absolute URLs, on purpose
98
+
99
+ Every URL in this block is written in full, with the scheme and the domain.
100
+
101
+ **Facebook, WhatsApp and X discard images with a relative path.** If `og:image` were `images/og-image.jpg`, there would be no thumbnail — no error, no warning, just a grey rectangle. The same applies to `canonical` and `og:url`.
102
+
103
+ The places the domain appears: `canonical`, `og:url`, `og:image`, `og:image:secure_url`, `twitter:image`, and `url` / `logo` / `image` inside the JSON-LD. That is a lot of repetition of one string, which is exactly why the head belongs in a parameterized include with the domain defined once (`assets/head.php`, `assets/social-meta.blade.php`).
104
+
105
+ ## Per-page values
106
+
107
+ The tags that must change per page: `<title>`, `description`, `canonical`, `og:url`, `og:title`, `og:description`. The ones that can stay site-wide: `og:site_name`, `og:locale`, `og:image` (with a per-page override where it is worth it), the icons, `theme-color`.
108
+
109
+ In Laravel, that split is `@props` with defaults on the component. In a static site, variables set before the include. Either way, a page that only needs a title and a description should be able to say just that.
@@ -0,0 +1,86 @@
1
+ # The images the platforms fetch
2
+
3
+ Three files do all the work: the share card, the favicon, and the iOS home-screen icon. Each has a constraint that is not obvious and fails silently when you miss it.
4
+
5
+ ---
6
+
7
+ ## `og:image` — the share card
8
+
9
+ **1200 × 630 px, JPEG.** That is the ratio every platform lays out for; anything else gets cropped by whoever is rendering it, and you do not get to choose where.
10
+
11
+ **JPEG, not WebP.** Several preview clients still cannot decode WebP and simply show no image. The 30 KB WebP would save are not worth an invisible link. This is the one place in a modern site where JPEG is still the right answer — the page itself can serve WebP.
12
+
13
+ **Under ~1.5 MB, ideally under 300 KB.** WhatsApp gives up on slow or heavy images and renders the card without a thumbnail.
14
+
15
+ What goes on it: the logo, a short claim, and one photograph or a solid brand background. It is read at postage-stamp size in a chat list — a full paragraph of text is unreadable there. Do not put anything important in the outer ~60 px, since some clients crop to a squarer ratio.
16
+
17
+ ```bash
18
+ # From a source photo: cover-crop to 1200x630, no upscaling beyond what it has
19
+ magick source.jpg -resize 1200x630^ -gravity center -extent 1200x630 \
20
+ -quality 82 -strip og-image.jpg
21
+ ```
22
+
23
+ `-resize 1200x630^` fills the box (the `^` means "at least"), `-extent` crops the overflow, and `-strip` removes EXIF, which on a phone photo can carry GPS coordinates you did not mean to publish.
24
+
25
+ If it carries text over a photo, add `-sampling-factor 4:4:4` — the default chroma subsampling smears coloured text at small sizes.
26
+
27
+ **Verify the file, do not trust the command.** `og:image:width` and `og:image:height` must equal what the file really measures; the audit script reads the dimensions from the file header for exactly this reason.
28
+
29
+ ```bash
30
+ magick identify og-image.jpg # → og-image.jpg JPEG 1200x630 …
31
+ ```
32
+
33
+ ## Favicon — one SVG
34
+
35
+ ```html
36
+ <link rel="icon" type="image/svg+xml" href="/images/logo-symbol.svg">
37
+ ```
38
+
39
+ **An SVG has no intrinsic size**, so it draws sharp at 16 px in the tab and at 512 px in a bookmark grid. A `.ico` or a PNG is fixed: the browser asks for it at half a dozen different sizes — tab, bookmarks bar, reading list, home screen — and scales whatever it finds, which is where blurry favicons come from. Safari has supported `rel="icon"` with SVG since version 15 (2021); Chrome and Firefox for longer.
40
+
41
+ Two things about the SVG file itself:
42
+
43
+ - **It must declare real dimensions, not `width="100%" height="100%"`.** A percentage-sized SVG has no intrinsic ratio, and a browser using it as an icon does not know what shape to give it. Set `width` and `height` to the `viewBox` values in pixels.
44
+ - **Pure vectors.** An SVG with an embedded raster or a non-outlined font is a large file that renders inconsistently. Outline the text.
45
+
46
+ Add a `favicon.ico` in the document root only if the site must support very old browsers; modern ones stop at the SVG. Do not spend time on a folder of eight PNG sizes — that convention predates SVG favicon support.
47
+
48
+ ## `apple-touch-icon` — 180 × 180 PNG
49
+
50
+ iOS **ignores the SVG** when the site is added to the home screen. With no PNG it screenshots the page, which at that size is a grey smudge.
51
+
52
+ ```bash
53
+ # From the same SVG, at a size that downsamples cleanly
54
+ rsvg-convert -w 1480 logo-symbol.svg -o /tmp/symbol.png
55
+ magick /tmp/symbol.png -resize 148x148 -background white -alpha remove \
56
+ -gravity center -extent 180x180 -strip apple-touch-icon.png
57
+ ```
58
+
59
+ Three decisions inside that command:
60
+
61
+ - **White background, no alpha.** iOS does not honour the alpha channel on these icons — it fills transparency with black, and a dark logo vanishes into it. `-alpha remove` flattens onto `-background`.
62
+ - **The symbol occupies 148 of 180 (~82%).** iOS applies its own rounded mask; a logo that reaches the edge loses its corners to it.
63
+ - **Square and unrounded.** iOS draws the corners. Rounding it yourself produces a double-rounded icon with white notches.
64
+
65
+ 180 × 180 is the Retina iPhone size; iOS downscales it for everything else. One file is enough — the `sizes` attribute is a hint, not a requirement to ship every size.
66
+
67
+ If there is no vector source, render from the largest raster available and accept it; do not upscale a 64 px PNG to 180 and call it done.
68
+
69
+ ## After changing any of them
70
+
71
+ Facebook and WhatsApp **cache the card for days**. Change the `og:image` and the link keeps showing the old one, which reads exactly like the fix not working. Force the refresh at <https://developers.facebook.com/tools/debug/> by pasting the URL and pressing *Scrape Again*.
72
+
73
+ If the image filename stays the same and the server sends a long `Cache-Control`, browsers hold the old one too. The discipline that replaces a short cache is **renaming the file when its content changes**.
74
+
75
+ ## Page images, for completeness
76
+
77
+ Not part of the share card, but the same audit usually finds them:
78
+
79
+ | Use | Format | Why |
80
+ | --- | --- | --- |
81
+ | Photographs | WebP, quality ~80 | 15–24× smaller than the equivalent PNG |
82
+ | Files meant to be shared or downloaded | JPEG `4:4:4` | they travel through WhatsApp; no chroma subsampling because they carry text |
83
+ | Logos and icons | SVG | sharp at any size, and doubles as the favicon |
84
+ | `og:image` | JPEG | several preview clients still cannot read WebP |
85
+
86
+ Below-the-fold images take `loading="lazy"`. Every image takes a descriptive `alt` — it is what a screen reader announces and what Google reads.
@@ -0,0 +1,77 @@
1
+ # Registering the site and getting it indexed
2
+
3
+ The tags and files make the site indexable. This is the part that tells the engines it exists. It needs a browser, a Google account and access to the domain's DNS — **the user has to do it**; hand them the sequence rather than pretending you can.
4
+
5
+ ---
6
+
7
+ ## Google Search Console
8
+
9
+ <https://search.google.com/search-console>
10
+
11
+ ### Choosing the property type
12
+
13
+ Google offers two, and they are not equivalent:
14
+
15
+ | Type | What it covers | How it is verified |
16
+ | --- | --- | --- |
17
+ | **Domain** | `example.com`, `www.`, http, https, and every subdomain | DNS only |
18
+ | URL prefix | exactly the address you type, and nothing else | HTML file, `<meta>` tag, DNS, Analytics… |
19
+
20
+ **Prefer Domain.** With URL prefix you would have to register each variant separately, which contradicts the whole point of having redirected them to one. Use URL prefix only when there is no access to the DNS zone.
21
+
22
+ Verification is a `TXT` record Google gives you, pasted into the domain's DNS zone at the registrar. Search Console calls this "Domain name provider". Propagation is usually minutes.
23
+
24
+ > **Never delete that TXT record.** If it disappears from the DNS, the verification is lost and the reports go with it. From *Settings → Ownership verification* you can add a second method as a backup.
25
+
26
+ ### Submitting the sitemap
27
+
28
+ Left menu → **Sitemaps**, under *Indexing*. On a Domain property the field wants the full URL:
29
+
30
+ ```
31
+ https://example.com/sitemap.xml
32
+ ```
33
+
34
+ It should land in **Success** state with the page count discovered. For the first few minutes it may say "Couldn't fetch"; that usually resolves itself.
35
+
36
+ This does not replace the `Sitemap:` line in `robots.txt` — they complement each other. Search Console tells Google directly; `robots.txt` serves every other crawler that comes by.
37
+
38
+ ### Requesting indexing
39
+
40
+ Top bar → **URL Inspection** → paste the home page URL.
41
+
42
+ It will say **"URL is not on Google"**, which is expected for a domain that just went live. Press **Request indexing**. That puts the page in the crawl queue instead of waiting for Google to arrive on its own. Do it for the home page and the two or three pages that matter most; the quota is limited and the sitemap covers the rest.
43
+
44
+ ### What to expect
45
+
46
+ The *Performance* and *Indexing* panels will show **"Processing data, please check back tomorrow"**. That is normal — first data takes between **3 days and 2 weeks**. It is not a configuration error, and re-submitting does not speed it up.
47
+
48
+ ## Bing Webmaster Tools
49
+
50
+ <https://www.bing.com/webmasters>
51
+
52
+ Worth ten minutes: it also feeds DuckDuckGo and, increasingly, the AI assistants that use Bing's index. It offers **import from Google Search Console**, which carries over the property and the verification in a couple of clicks — do that instead of repeating the DNS dance.
53
+
54
+ Then submit the same sitemap URL. Its **IndexNow** feature accepts an explicit ping when a page changes, which is useful for a site publishing several times a day.
55
+
56
+ ## Validators, once it is live
57
+
58
+ | Tool | What it tells you |
59
+ | --- | --- |
60
+ | <https://validator.schema.org/> | the JSON-LD is well-formed and its properties exist |
61
+ | <https://search.google.com/test/rich-results> | whether the page qualifies for a rich result (a different question — see `structured-data.md`) |
62
+ | <https://developers.facebook.com/tools/debug/> | how the card renders, and the button that busts the cache |
63
+ | <https://cards-dev.twitter.com/validator> | the X card, when it is reachable |
64
+ | <https://pagespeed.web.dev/> | Core Web Vitals and performance |
65
+ | <https://securityheaders.com/> | the headers from the `.htaccess` |
66
+
67
+ ## Facebook's cache
68
+
69
+ Facebook and WhatsApp cache the card for days. After changing an `og:` tag or the image, paste the URL into the **Sharing Debugger** and press *Scrape Again*. Without it, the link keeps showing the old card and it reads exactly like the change never deployed.
70
+
71
+ WhatsApp uses Facebook's crawler, so refreshing there fixes both. A trick that works when the cache will not budge: append a harmless query string (`?v=2`) to the URL you are testing to force a fresh scrape.
72
+
73
+ ## What none of this buys
74
+
75
+ Registering a site does not rank it. What Search Console gives you is visibility into what Google sees — which pages it indexed, which queries reach you, which URLs it refused and why. Indexing itself is a matter of days to weeks for a new domain, and no button shortens it.
76
+
77
+ If after two weeks the pages are still not indexed, the *Pages* report says why in plain language: `noindex` detected, crawl blocked by `robots.txt`, redirect, duplicate without a canonical, or "discovered but not indexed" — which usually means the content is too thin to be worth a slot.
@@ -0,0 +1,168 @@
1
+ # Server files: `robots.txt`, `sitemap.xml`, `.htaccess`
2
+
3
+ The three files that live in the document root. Put them one level above it and the work silently does nothing — check where Apache actually serves from before writing.
4
+
5
+ ---
6
+
7
+ ## `robots.txt`
8
+
9
+ ```
10
+ User-agent: *
11
+ Allow: /
12
+ Disallow: /send.php
13
+
14
+ Sitemap: https://example.com/sitemap.xml
15
+ ```
16
+
17
+ Its main job here is the **`Sitemap:` line**: it is the only standard place to point at the sitemap without submitting it by hand to each engine. The `Sitemap:` URL must be absolute.
18
+
19
+ `Disallow` is for endpoints that are not pages — a form handler, an internal API, a debug route. Do not disallow `/admin` and think it is protected: `robots.txt` is public and reads as a directory of interesting URLs to anyone curious. It keeps polite crawlers out; it is not access control.
20
+
21
+ **A missing `robots.txt` returns 404**, which crawlers accept as "everything is allowed" — so the site still gets indexed. What you lose is the sitemap pointer.
22
+
23
+ **Never ship `Disallow: /`** to production. It is the staging default and it removes the whole site from the index.
24
+
25
+ ## `sitemap.xml`
26
+
27
+ ```xml
28
+ <?xml version="1.0" encoding="UTF-8"?>
29
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
30
+ <url>
31
+ <loc>https://example.com/</loc>
32
+ <lastmod>2026-08-13</lastmod>
33
+ <changefreq>monthly</changefreq>
34
+ <priority>1.0</priority>
35
+ </url>
36
+ <url>
37
+ <loc>https://example.com/faq.html</loc>
38
+ <lastmod>2026-08-13</lastmod>
39
+ <priority>0.6</priority>
40
+ </url>
41
+ </urlset>
42
+ ```
43
+
44
+ - **URLs, not fragments.** The sections of a single page (`#services`, `#contact`) are anchors in the same document and do not go in. A sitemap indexes documents.
45
+ - **Only canonical URLs.** Listing both `www.` and the apex, or both `http` and `https`, contradicts the redirects and the canonical tag.
46
+ - **`lastmod` must be truthful.** It is the signal that tells a crawler to come back; a file that stamps today's date on every page every day gets its `lastmod` ignored entirely.
47
+ - **`changefreq` and `priority` are advisory** and Google largely disregards them. They are harmless. `priority` is relative *within your site*, so making everything 1.0 says nothing.
48
+ - Serve it as `application/xml`. If the server sends `text/plain`, add the MIME type (below).
49
+
50
+ For a site with dozens of pages, write it by hand or generate it in the build. For a database-driven site (Laravel, WordPress), generate it from a route or a scheduled command — a hand-written sitemap of a news site is stale the day it ships.
51
+
52
+ ## `.htaccess`
53
+
54
+ For a **static site** served directly by Apache. A Laravel project already has an `.htaccess` with the front-controller rewrite: add these blocks to it, do not replace it. Full commented template in `assets/htaccess-static`.
55
+
56
+ ### One canonical domain
57
+
58
+ Before this, three URLs typically serve the same content with a 200: `http://example.com/`, `https://www.example.com/`, `https://example.com/`. For a search engine that is duplicate content, and the authority is split across the three.
59
+
60
+ ```apache
61
+ RewriteEngine On
62
+
63
+ RewriteCond %{HTTPS} !=on
64
+ RewriteCond %{HTTP:X-Forwarded-Proto} !=https
65
+ RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]
66
+
67
+ RewriteCond %{HTTP_HOST} ^www\.example\.com$ [NC]
68
+ RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]
69
+ ```
70
+
71
+ **The double HTTPS condition is not redundant.** Behind a proxy or CDN — which is most shared hosting — `%{HTTPS}` can arrive off even though the request was secure. Testing only that variable produces an infinite redirect loop.
72
+
73
+ Pick apex or `www` and be consistent with the canonical tag and the sitemap. Which one does not matter; disagreeing about it does.
74
+
75
+ ### Caching
76
+
77
+ ```apache
78
+ <IfModule mod_headers.c>
79
+ <FilesMatch "\.(webp|jpe?g|png|svg|ico|woff2)$">
80
+ Header set Cache-Control "public, max-age=31536000, immutable"
81
+ </FilesMatch>
82
+
83
+ <FilesMatch "\.(html|css|js)$">
84
+ Header set Cache-Control "public, max-age=0, must-revalidate"
85
+ </FilesMatch>
86
+ </IfModule>
87
+ ```
88
+
89
+ **Images and fonts: a year, `immutable`** — the browser will not even ask whether they changed. The discipline that makes this safe is renaming a file when its content changes.
90
+
91
+ **HTML and CSS: revalidate every time.** This is the counter-intuitive half, and it is the one that bites. A CSS file that is always called `app.css` cannot be cached long: recompile it, and every returning visitor keeps the old one until the cache expires — new utility classes silently missing, no console error, the page just looks broken for the people who visited before. A revalidation costs a 304 with an empty body; a month of stale CSS costs a bug you cannot reproduce on your own machine.
92
+
93
+ That trade only flips when the build fingerprints filenames (`app.4f3a1c.css`) and rewrites the references, which is what Vite and friends do — then the CSS is immutable like the images.
94
+
95
+ ### Compression
96
+
97
+ ```apache
98
+ <IfModule mod_deflate.c>
99
+ AddOutputFilterByType DEFLATE text/html text/css text/plain text/xml \
100
+ application/javascript application/json image/svg+xml
101
+ </IfModule>
102
+ ```
103
+
104
+ `image/svg+xml` is the one usually missing from a default config. An SVG is text and compresses enormously — a 17 KB logo drops to 6 KB.
105
+
106
+ ### Security headers
107
+
108
+ ```apache
109
+ <IfModule mod_headers.c>
110
+ Header always set X-Content-Type-Options "nosniff"
111
+ Header always set Referrer-Policy "strict-origin-when-cross-origin"
112
+ Header always set X-Frame-Options "SAMEORIGIN"
113
+ Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
114
+ </IfModule>
115
+ ```
116
+
117
+ | Header | What it prevents |
118
+ | --- | --- |
119
+ | `nosniff` | the browser guessing a file's type and executing something that is not a script |
120
+ | `Referrer-Policy` | leaking the full originating URL when the user follows a link out |
121
+ | `X-Frame-Options` | the site being framed for clickjacking |
122
+ | `Permissions-Policy` | the page requesting camera, microphone or location |
123
+
124
+ **`Content-Security-Policy` is deliberately not in the default set.** A miscalibrated CSP strips the site of its styles, and any page with an inline `<script>` or a third-party font needs it tuned per site. Add it once those are self-hosted and you can test it; shipping a broken one is worse than shipping none.
125
+
126
+ ### Hidden files and working files
127
+
128
+ ```apache
129
+ RedirectMatch 404 /\.(?!well-known/)
130
+
131
+ <FilesMatch "\.(md|json|lock|yml|yaml|sh|sql|bak|log)$">
132
+ Require all denied
133
+ </FilesMatch>
134
+ ```
135
+
136
+ Any path starting with a dot returns 404. This matters more than it sounds: an SFTP watcher that uploads the project directory can put `.vscode/sftp.json` — with the hosting password in plain text — inside the document root, where it is one guessed URL away from anyone.
137
+
138
+ **The `(?!well-known/)` exception is not optional.** AutoSSL renews the certificate by placing a file in `/.well-known/acme-challenge/`. Without the exception the validation fails silently and you find out when the certificate expires. Test it by serving a real file from that path before calling the rule done.
139
+
140
+ The real fix for that class of problem is upstream — the document root should be a subdirectory (`public/`) with the repository and its config living above it, and the deploy tool's ignore list should exclude `.vscode`, `.git` and `node_modules`. The `.htaccess` rule is the second barrier, not the first.
141
+
142
+ ### MIME types
143
+
144
+ ```apache
145
+ AddType image/svg+xml .svg
146
+ AddType image/webp .webp
147
+ AddType application/xml .xml
148
+ ```
149
+
150
+ An SVG served as `text/plain` renders **blank**. If the logo and the favicon are SVG, this line is the difference between a site with a logo and a site without one.
151
+
152
+ ## Verifying, from the terminal
153
+
154
+ ```bash
155
+ # Redirects: expect 301 and the canonical target
156
+ curl -so /dev/null -w '%{http_code} → %{redirect_url}\n' http://example.com/
157
+ curl -so /dev/null -w '%{http_code} → %{redirect_url}\n' https://www.example.com/
158
+
159
+ # Headers on an image (cache) and on the HTML (security, revalidation)
160
+ curl -sI https://example.com/images/hero.webp
161
+ curl -sI https://example.com/
162
+
163
+ # Something that must not be reachable
164
+ curl -sI https://example.com/CLAUDE.md | head -1
165
+
166
+ # Real transferred weight, compressed
167
+ curl -s --compressed -o /dev/null -w '%{size_download}\n' https://example.com/
168
+ ```