Compare commits
2
Commits
171c8c96aa
...
5bfc5981cd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bfc5981cd | ||
|
|
4e354d2721 |
No files matched your search
+54
-18
@@ -26,6 +26,7 @@ Status legend: `[ ]` open · `[x]` done · `[?]` needs verification · 🛑 owne
|
|||||||
| `npm run seo:lint` | green, 21 pages clean |
|
| `npm run seo:lint` | green, 21 pages clean |
|
||||||
| `npm run test:e2e` | **42 passed / 4 failed** (stale assertions, see §2.1) |
|
| `npm run test:e2e` | **42 passed / 4 failed** (stale assertions, see §2.1) |
|
||||||
| CI root cause | **found + fixed in-repo 2026-09-22** — see §1.2; needs a green run to confirm |
|
| CI root cause | **found + fixed in-repo 2026-09-22** — see §1.2; needs a green run to confirm |
|
||||||
|
| Staging URL | **https://bcs.int.7tl-homelab.com** — LAN-only, valid cert (§2.6) |
|
||||||
| Old WordPress site | untouched and serving — must stay that way until cutover |
|
| Old WordPress site | untouched and serving — must stay that way until cutover |
|
||||||
| Gitea host | KVM VM `gitea-1` (10.66.15.22), **Debian 12**, docker compose — *not* in k3s |
|
| Gitea host | KVM VM `gitea-1` (10.66.15.22), **Debian 12**, docker compose — *not* in k3s |
|
||||||
| Gitea / act_runner | 1.27.3 / 3.4.2 — both healthy |
|
| Gitea / act_runner | 1.27.3 / 3.4.2 — both healthy |
|
||||||
@@ -44,10 +45,14 @@ Nothing below is optional. Cutover is unsafe until all of §1 is closed.
|
|||||||
- [ ] Uncomment the `tls:` block and `cert-manager.io/cluster-issuer` annotation in
|
- [ ] Uncomment the `tls:` block and `cert-manager.io/cluster-issuer` annotation in
|
||||||
`k8s/ingress.yaml`; set `secretName: bluecap-website-tls`.
|
`k8s/ingress.yaml`; set `secretName: bluecap-website-tls`.
|
||||||
- [ ] Use the existing `letsencrypt-prod` ClusterIssuer (present in-cluster, 472d old, Ready).
|
- [ ] Use the existing `letsencrypt-prod` ClusterIssuer (present in-cluster, 472d old, Ready).
|
||||||
- [ ] **Resolve the ordering problem:** HTTP-01 validation requires DNS to already point at the
|
- [x] **Ordering problem SOLVED.** `letsencrypt-prod` solves via **DNS-01 against Cloudflare**,
|
||||||
cluster, but we don't want to move DNS before a cert exists. Pick one:
|
not HTTP-01, so a cert can be issued *before* any DNS points at the cluster — no insecure
|
||||||
- DNS-01 issuer (preferred — issues the cert *before* cutover, zero insecure window), or
|
window. Proven end to end on 2026-09-23: the staging host issued a valid LE cert in ~75s
|
||||||
- accept a short window: cut DNS, then let HTTP-01 solve, serving plain HTTP for ~1–2 min.
|
with no public reachability (§2.6).
|
||||||
|
- [ ] 🛑 **Verify the one remaining dependency:** `bluecapstrategies.com` must be in the
|
||||||
|
Cloudflare account that cert-manager's API token can reach. The domain is at Bluehost
|
||||||
|
today, so this is probably **not** yet true. It is the single fact separating a clean
|
||||||
|
zero-downtime cutover from one with a gap — check it early, not on launch day.
|
||||||
- [ ] Verify `https://` serves a valid chain and HTTP→HTTPS redirects before announcing.
|
- [ ] Verify `https://` serves a valid chain and HTTP→HTTPS redirects before announcing.
|
||||||
|
|
||||||
### 1.2 CI/CD is stalled — no deploy since 2026-07-20 🛑 hard blocker
|
### 1.2 CI/CD is stalled — no deploy since 2026-07-20 🛑 hard blocker
|
||||||
@@ -111,11 +116,13 @@ Remaining:
|
|||||||
uses apt at all, and the job image is no longer EOL (bookworm still serves `docker.io` fine). The
|
uses apt at all, and the job image is no longer EOL (bookworm still serves `docker.io` fine). The
|
||||||
static-binary approach remains the more durable of the two; keep it regardless.
|
static-binary approach remains the more durable of the two; keep it regardless.
|
||||||
|
|
||||||
### 1.3 Developer-facing copy is visible on the live contact page 🛑
|
### 1.3 Developer-facing copy on the contact page ✅ FIXED 2026-09-23
|
||||||
`src/pages/contact.astro:37-41` renders, to real visitors:
|
Two notes written for developers, not visitors, were being rendered:
|
||||||
> "The form is static-first and posts to the configured endpoint when
|
- the form note explaining `PUBLIC_CONTACT_FORM_ENDPOINT`, and
|
||||||
> `PUBLIC_CONTACT_FORM_ENDPOINT` is set. Without an endpoint, it opens an email…"
|
- a "Discovery note" about the WordPress page holding placeholder contact values.
|
||||||
- [ ] Delete that `.form-note`. Replace with visitor-appropriate copy or nothing.
|
|
||||||
|
Replaced the first with reader-facing copy and dropped the second (the deviation it described is
|
||||||
|
already recorded in `docs/content-deviations.md`). Commit `4e354d2`.
|
||||||
|
|
||||||
### 1.4 Contact form has no endpoint 🛑
|
### 1.4 Contact form has no endpoint 🛑
|
||||||
`PUBLIC_CONTACT_FORM_ENDPOINT` is `""` in `k8s/configmap.yaml`, so the form falls back to a
|
`PUBLIC_CONTACT_FORM_ENDPOINT` is `""` in `k8s/configmap.yaml`, so the form falls back to a
|
||||||
@@ -124,15 +131,15 @@ mailto handoff. Inbound leads will be lost or awkward on day one.
|
|||||||
- [ ] Test a real submission end-to-end and confirm `/thank-you/` is reached.
|
- [ ] Test a real submission end-to-end and confirm `/thank-you/` is reached.
|
||||||
- [ ] Confirm the delivery address is monitored.
|
- [ ] Confirm the delivery address is monitored.
|
||||||
|
|
||||||
### 1.5 Placeholder blog post would go live and be indexed 🛑
|
### 1.5 Draft insight was being published and indexed ✅ FIXED 2026-09-23
|
||||||
`src/content/insights/example-draft.md` has `draft: true`, but the flag is **declared in the Zod
|
`src/pages/insights/[slug].astro` called `getCollection("insights")` unfiltered, so the
|
||||||
schema and never enforced** — no `filter` on `getCollection("insights")` in
|
`draft: true` placeholder built to `/insights/example-draft/` **and** appeared in
|
||||||
`src/pages/insights/[slug].astro` or the index. Result: `/insights/example-draft/` is built *and*
|
`sitemap-0.xml`. The schema declared the flag and `rss.xml.ts` already honoured it — only the page
|
||||||
listed in `dist/sitemap-0.xml`, so "Example draft insight" ships to Google.
|
route ignored it. Filtered in `getStaticPaths`, matching the RSS pattern. Build is 21 → 20 pages
|
||||||
- [ ] Either filter drafts out of `getCollection` (both the index and `getStaticPaths`) — the
|
and the URL is gone from the sitemap. Commit `4e354d2`.
|
||||||
durable fix — or delete the file.
|
|
||||||
- [ ] Re-run `npm run build` and confirm the URL is gone from `sitemap-0.xml`.
|
- [ ] 🛑 **Still open (owner):** whether `/insights/` should ship at all with zero real articles.
|
||||||
- [ ] Decide whether `/insights/` should ship at all with zero real articles (see §3).
|
It is currently in the primary nav. See also §2.5.
|
||||||
|
|
||||||
### 1.6 Canonical host is undecided 🛑 owner
|
### 1.6 Canonical host is undecided 🛑 owner
|
||||||
The ingress answers on **both** apex and `www`; `astro.config.mjs`, `robots.txt`, and the
|
The ingress answers on **both** apex and `www`; `astro.config.mjs`, `robots.txt`, and the
|
||||||
@@ -146,6 +153,35 @@ hosts with no redirect splits SEO signal from day one.
|
|||||||
|
|
||||||
## 2. Should-fix before cutover
|
## 2. Should-fix before cutover
|
||||||
|
|
||||||
|
### 2.5 `/insights/` index still renders developer-facing copy
|
||||||
|
Found while fixing §1.5. `src/pages/insights/index.astro` shows visitors two cards written for a
|
||||||
|
developer, not a reader:
|
||||||
|
- *"Publishing path"* — "Future posts can be added as Markdown or MDX in
|
||||||
|
`src/content/insights` with typed frontmatter for title, description, publish date…"
|
||||||
|
- *"RSS support"* — "The feed is ready for future article imports even though no public posts are
|
||||||
|
published yet."
|
||||||
|
|
||||||
|
The hero also announces the section as a "Future publishing home", i.e. it tells visitors the
|
||||||
|
section is empty. Same defect class as §1.3, but left alone deliberately: what the page *should*
|
||||||
|
say depends on the owner's call in §1.5 on whether `/insights/` ships at all.
|
||||||
|
|
||||||
|
- [ ] If `/insights/` ships: delete both note-cards and rewrite the hero as reader-facing copy.
|
||||||
|
- [ ] If it does not: drop it from the primary nav and exclude the route from the sitemap.
|
||||||
|
|
||||||
|
### 2.6 Staging URL ✅ LIVE 2026-09-23
|
||||||
|
**https://bcs.int.7tl-homelab.com** — review the rebuild in a browser, valid cert, no warning.
|
||||||
|
|
||||||
|
- LAN-only: `*.int.7tl-homelab.com` resolves to the internal traefik LB (10.66.15.30). Not
|
||||||
|
reachable from the internet; the real domain is untouched and WordPress still serves it.
|
||||||
|
- Cert issued by Let's Encrypt via cert-manager DNS-01 in ~75s, `CN=bcs.int.7tl-homelab.com`,
|
||||||
|
valid to 2026-12-21, auto-renewing.
|
||||||
|
- All 9 public routes verified 200.
|
||||||
|
- Commit `171c8c9`. Note it also corrected the issuer name in `k8s/ingress.yaml`: the commented
|
||||||
|
placeholder said `letsencrypt-production`, but the ClusterIssuer is **`letsencrypt-prod`** —
|
||||||
|
uncommenting it as written at cutover would have silently failed to issue.
|
||||||
|
- ArgoCD may take a few minutes to poll a push; force it with
|
||||||
|
`kubectl annotate application bcs-website -n argocd argocd.argoproj.io/refresh=hard --overwrite`.
|
||||||
|
|
||||||
### 2.1 e2e suite is red — stale assertions, not a regression
|
### 2.1 e2e suite is red — stale assertions, not a regression
|
||||||
4 of 46 tests fail. Both failures assert **invented copy that the exact-copy work correctly
|
4 of 46 tests fail. Both failures assert **invented copy that the exact-copy work correctly
|
||||||
removed**:
|
removed**:
|
||||||
|
|||||||
@@ -27,17 +27,11 @@ const endpoint = import.meta.env.PUBLIC_CONTACT_FORM_ENDPOINT ?? "";
|
|||||||
<h2>Contact Info</h2>
|
<h2>Contact Info</h2>
|
||||||
<p><strong>Phone</strong><br /><a href={`tel:${site.phone.replaceAll("-", "")}`}>{site.phone}</a></p>
|
<p><strong>Phone</strong><br /><a href={`tel:${site.phone.replaceAll("-", "")}`}>{site.phone}</a></p>
|
||||||
<p><strong>Email</strong><br /><a href={site.contactEmailHref}>{site.email}</a></p>
|
<p><strong>Email</strong><br /><a href={site.contactEmailHref}>{site.email}</a></p>
|
||||||
<p class="form-note">
|
|
||||||
Discovery note: the current WordPress contact page includes placeholder phone and email
|
|
||||||
values. This rebuild uses the verified homepage/footer contact details.
|
|
||||||
</p>
|
|
||||||
</aside>
|
</aside>
|
||||||
<div class="form-panel">
|
<div class="form-panel">
|
||||||
<h2>Request a consultation</h2>
|
<h2>Request a consultation</h2>
|
||||||
<p class="form-note">
|
<p class="form-note">
|
||||||
The form is static-first and posts to the configured endpoint when
|
Tell us about your project and we will follow up within two business days.
|
||||||
<code>PUBLIC_CONTACT_FORM_ENDPOINT</code> is set. Without an endpoint, it opens an email
|
|
||||||
draft after validating required fields.
|
|
||||||
</p>
|
</p>
|
||||||
<form class="contact-form" method="post" action={endpoint || "/thank-you/"} data-contact-form data-endpoint={endpoint}>
|
<form class="contact-form" method="post" action={endpoint || "/thank-you/"} data-contact-form data-endpoint={endpoint}>
|
||||||
<input type="hidden" name="subject" value="BlueCap Strategies website inquiry" />
|
<input type="hidden" name="subject" value="BlueCap Strategies website inquiry" />
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import ContactCta from "@components/ContactCta.astro";
|
|||||||
import { breadcrumbSchema, pageSchema } from "@utils/schema";
|
import { breadcrumbSchema, pageSchema } from "@utils/schema";
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const posts = await getCollection("insights");
|
// Drafts must never reach the build: an unfiltered getCollection() published
|
||||||
|
// /insights/example-draft/ AND listed it in the sitemap. Matches rss.xml.ts.
|
||||||
|
const posts = await getCollection("insights", ({ data }) => !data.draft);
|
||||||
return posts.map((post) => ({
|
return posts.map((post) => ({
|
||||||
params: { slug: post.slug },
|
params: { slug: post.slug },
|
||||||
props: { post },
|
props: { post },
|
||||||
|
|||||||
Reference in new issue
Block a user