Fixed various issues
Some checks failed
Deploy / cleanup-preview (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

This commit is contained in:
2026-05-24 20:30:02 -05:00
parent 5e05a7c256
commit cc51aef447
11 changed files with 323 additions and 82 deletions

5
.env.example Normal file
View File

@@ -0,0 +1,5 @@
# Contact form / mail (Mailgun). Remove if not using Mailgun.
MAILGUN_API_KEY=
MAILGUN_DOMAIN=
MAILGUN_FROM="Site Name <no-reply@example.com>"
CONTACT_TO="Recipient Name <recipient@example.com>"

181
README.md
View File

@@ -1,5 +1,186 @@
# Astro Project Starter Template
An opinionated Astro starter for Arachne web agency client builds. Optimized
for accessibility, SEO, AI/LLM readability, Google Business / local search,
and webcrawler friendliness out of the box.
```sh
npm create astro@latest -- --template https://git.zyrrus/dev/arachne/astro-template
```
## Post-Clone Checklist
Work through these in order. Items marked **(required)** must be done before
the first production build; others can be deferred but should be completed
before launch.
### 1. Project Identity
- [ ] **(required)** Update [package.json](package.json) — set `name`, `version`, and remove any fields that don't apply.
- [ ] **(required)** Rewrite this README with project-specific info (or delete it).
- [ ] Remove or replace the agency clone command at the top of this file.
- [ ] Initialize a fresh git history if cloned via `git clone` instead of the Astro template flow: `rm -rf .git && git init`.
### 2. Environment Variables
- [ ] **(required)** Create `.env.example` documenting every variable the project needs. Commit it.
- [ ] **(required)** Copy `.env.example``.env` and fill in real values. Do **not** commit `.env` (already in [.gitignore](.gitignore)).
- [ ] **(required)** Audit the existing [.env](.env) — the template ships with placeholder Mailgun keys. Rotate any committed credentials immediately.
- [ ] **(required)** Update the `env.schema` block in [astro.config.mjs](astro.config.mjs) to match the variables the project actually uses. Remove any unused entries (default ships with Mailgun + contact-form vars).
- [ ] If using a different mailer / form backend, swap or remove the Mailgun env vars entirely.
### 3. Astro Config
- [ ] **(required)** Set `site: "https://example.com"` in [astro.config.mjs](astro.config.mjs) — this is required for `@astrojs/sitemap`, canonical URLs, and absolute OG image URLs to work.
- [ ] Pick the right output mode in [astro.config.mjs](astro.config.mjs): keep `output: "server"` for SSR (forms, API routes, dynamic content) or change to `"static"` for fully static builds. Drop `@astrojs/node` if going static.
- [ ] Remove `@astrojs/preact` if the project won't use Preact islands.
- [ ] Add any extra integrations needed (e.g. `@astrojs/mdx`, `@astrojs/image`).
### 4. Site Content (`site.yaml`)
The template ships with sample data for a law firm — replace **all** of it.
- [ ] **(required)** Replace every field in [src/content/site.yaml](src/content/site.yaml) with the new client's info: `name`, `shortName`, `url`, `tagline`, `description`, `phone`, `email`, `address`, `hours`, `nav`.
- [ ] **(required)** Update the zod schema in [src/content.config.ts](src/content.config.ts) to match the shape you actually use. The shipped schema is intentionally minimal; the YAML has extra fields (offices, fax, affiliate, established) that you may want to either remove from YAML or add to the zod schema for type safety.
- [ ] Decide on multi-office vs single-office. The shipped YAML has both `offices[]` and a top-level `address` — pick one, update [src/lib/schema.ts](src/lib/schema.ts) accordingly.
- [ ] Review [src/lib/schema.ts](src/lib/schema.ts) — it hardcodes `"LegalService"` as the schema.org type and `"Louisiana"` as `areaServed`. Change `@type` to the appropriate [schema.org type](https://schema.org/docs/full.html) (`LocalBusiness`, `ProfessionalService`, `Restaurant`, etc.) and update `areaServed`.
- [ ] Add JSON-LD for any extra entity types the project needs (`Person` for staff, `Service` for offerings, `FAQPage`, `Article`, `Product`) — extend [src/lib/schema.ts](src/lib/schema.ts).
### 5. Base Layout Cleanup
In [src/layouts/base-layout.astro](src/layouts/base-layout.astro):
- [ ] **(required)** Line 73: replace hardcoded `og:site_name="Armor Title Company"` with `{site.name}`.
- [ ] Line 37: replace `lang="en"` with the correct locale, or wire it to a site-level config.
- [ ] Line 41: change `<meta name="description" content={description} />` to use `pageDesc` so the fallback works.
- [ ] Line 74: replace hardcoded `og:locale="en_US"` if the project isn't US English.
- [ ] Add a `<meta name="theme-color">` matching the brand color.
- [ ] Add `twitter:site` / `twitter:creator` handles if the client has a Twitter/X presence.
### 6. Branding — Colors, Fonts, Tokens
- [ ] **(required)** Add the brand palette to [src/styles/global.css](src/styles/global.css) inside `@theme { }`. Tailwind 4 reads CSS custom properties as tokens — define `--color-*`, `--font-*`, `--radius-*`, etc. Example:
```css
@theme {
--color-bg: oklch(98% 0.01 80);
--color-fg: oklch(20% 0.02 80);
--color-accent: oklch(60% 0.15 250);
--font-sans: var(--font-inter), system-ui, sans-serif;
}
```
- [ ] **(required)** Wire the brand font in [astro.config.mjs](astro.config.mjs) `fonts: [...]`. Default is Inter from Google — swap `name`, `weights`, `styles`, and `cssVariable` for the project's font. Then update `--font-*` in [src/styles/global.css](src/styles/global.css) and the `<Font cssVariable="..." preload />` call in [src/layouts/base-layout.astro](src/layouts/base-layout.astro) to match.
- [ ] Fill in the three `TODO` comments in [src/components/skip-link.astro](src/components/skip-link.astro) with real `--color-*` tokens once defined.
- [ ] Set base `body` font + background colors in [src/styles/global.css](src/styles/global.css).
### 7. Favicons & App Icons
The base layout references favicon files that don't ship with the template
(only `favicon.ico` and `favicon.svg` exist). Generate the rest from the
brand mark.
- [ ] **(required)** Run a source mark through [realfavicongenerator.net](https://realfavicongenerator.net/) or similar.
- [ ] **(required)** Drop the generated files into [public/favicon/](public/favicon/). At minimum the base layout expects:
- `favicon.ico`
- `favicon.svg`
- `favicon-96x96.png`
- `apple-touch-icon.png` (180×180)
- `web-app-manifest-512x512.png`
- `site.webmanifest`
- [ ] Update `site.webmanifest` `name`, `short_name`, `theme_color`, `background_color`.
### 8. Open Graph / Social Images
- [ ] **(required)** Generate a default OG image (1200×630) and place it in [public/og/](public/og/).
- [ ] **(required)** Update the `ogImage` default in [src/layouts/base-layout.astro](src/layouts/base-layout.astro) (currently falls back to `/favicon/web-app-manifest-512x512.png` — not 1200×630, which violates the `og:image:width`/`height` meta tags).
- [ ] Consider per-page OG images: pass `ogImage` prop to `<BaseLayout>` per route, or generate them dynamically with [`@vercel/og`](https://vercel.com/docs/functions/og-image-generation) / Satori in an API route.
### 9. Pages & Routes
- [ ] **(required)** Replace [src/pages/index.astro](src/pages/index.astro) with the real homepage.
- [ ] **(required)** Add a `src/pages/404.astro` for proper 404 handling.
- [ ] Implement nav target pages to match `nav` entries in [src/content/site.yaml](src/content/site.yaml).
- [ ] Build out the stub components in [src/components/sections/](src/components/sections/), [src/components/primitives/](src/components/primitives/), and [src/components/composites/](src/components/composites/).
- [ ] If using the contact form endpoint, implement `src/pages/api/contact.ts` (or whatever `contactFormEndpoint` points to in `site.yaml`).
- [ ] Review [src/pages/robots.txt.ts](src/pages/robots.txt.ts) — adjust crawl rules if the site has private areas.
### 10. Accessibility Pass
The template gives you the bones; verify the build.
- [ ] All interactive elements reachable by keyboard; focus styles visible.
- [ ] Color contrast ≥ 4.5:1 for body text, ≥ 3:1 for large text and UI components (WCAG AA).
- [ ] Every `<img>` has meaningful `alt` (or `alt=""` for decorative).
- [ ] Heading hierarchy is sequential (no skipped levels).
- [ ] Forms have associated `<label>`s; errors are announced.
- [ ] Run [axe DevTools](https://www.deque.com/axe/devtools/) or Lighthouse a11y audit before launch.
### 11. SEO / Crawler / Google Business
- [ ] Verify `site` URL is set in [astro.config.mjs](astro.config.mjs) (sitemap and canonicals depend on it).
- [ ] Confirm `https://your-site/sitemap-index.xml` resolves after build.
- [ ] Submit the sitemap in [Google Search Console](https://search.google.com/search-console).
- [ ] Set up / claim the [Google Business Profile](https://business.google.com/) — make sure NAP (name, address, phone) in `site.yaml` exactly matches the GBP listing.
- [ ] Add `Person` schema entries for owners/staff if relevant.
- [ ] Validate JSON-LD output with [Google's Rich Results Test](https://search.google.com/test/rich-results).
- [ ] Add per-page `title` and `description` props on every `<BaseLayout>` usage.
### 12. AI / LLM Readability
- [ ] Use semantic HTML (`<article>`, `<section>`, `<nav>`, `<header>`, `<footer>`, `<address>`) — LLM scrapers weight this heavily.
- [ ] Keep JSON-LD comprehensive — it's the canonical structured representation for LLM ingestion.
- [ ] Consider adding `/llms.txt` (and `/llms-full.txt`) at the public root summarizing the site for LLM crawlers — see [llmstxt.org](https://llmstxt.org/).
- [ ] Avoid critical content rendered only via client-side JS; this template defaults to SSR for a reason.
### 13. Deployment
The template ships with a [Gitea Actions workflow](.gitea/workflows/deploy.yml) that deploys via Pangolin.
- [ ] **(required)** Configure these Gitea repo **secrets**: `PANGOLIN_API_URL`, `PANGOLIN_API_KEY`, `PANGOLIN_ORG_ID`, `PANGOLIN_DOMAIN_ID`, `PANGOLIN_SITE_ID`, `PANGOLIN_TARGET_IP`, `MAILGUN_API_KEY`, `MAILGUN_DOMAIN`, `CONTACT_TO`, `MAILGUN_FROM`.
- [ ] **(required)** Configure repo **variable** `APP_NAME`.
- [ ] Update the `env-vars:` block in the workflow if the project uses different env vars than the Mailgun defaults.
- [ ] If not deploying via Gitea/Pangolin, delete [.gitea/](/.gitea/) and add the right CI config for your target (Vercel, Netlify, fly.io, raw Docker, etc.).
- [ ] Review the [dockerfile](dockerfile) — it assumes `output: "server"`. For static builds, switch to an Nginx-based image.
### 14. Final Pre-Launch Checks
- [ ] `npm run build` succeeds with no warnings.
- [ ] Lighthouse scores: Performance ≥ 90, Accessibility 100, SEO 100, Best Practices ≥ 95.
- [ ] Test OG preview with [OpenGraph.xyz](https://www.opengraph.xyz/).
- [ ] Test Twitter card with the [Card Validator](https://cards-dev.twitter.com/validator).
- [ ] Verify `robots.txt` and `sitemap-index.xml` at the deployed URL.
- [ ] Verify favicons render correctly across browsers + iOS home screen.
- [ ] Click through every nav link and form on mobile + desktop.
## Project Structure
```
.
├── .gitea/workflows/ # Gitea CI/CD (Pangolin deploy)
├── public/
│ ├── favicon/ # Favicons + web manifest
│ └── og/ # Open Graph images
├── src/
│ ├── assets/ # Imported assets (optimized at build time)
│ ├── components/
│ │ ├── primitives/ # Buttons, inputs — atoms
│ │ ├── composites/ # Accordion, marquee — molecules
│ │ ├── sections/ # Header, footer, page sections
│ │ ├── seo-json-ld.astro
│ │ └── skip-link.astro
│ ├── content/
│ │ └── site.yaml # Single source of truth for site metadata
│ ├── layouts/
│ │ └── base-layout.astro # All <head>, SEO, fonts, schemas live here
│ ├── lib/
│ │ ├── schema.ts # JSON-LD generators
│ │ └── site.ts # Typed accessor for site.yaml
│ ├── pages/
│ │ ├── index.astro
│ │ └── robots.txt.ts
│ ├── styles/
│ │ └── global.css # Tailwind import + @theme tokens
│ └── content.config.ts # Content collection schema (zod)
├── astro.config.mjs
├── dockerfile
└── tsconfig.json
```

View File

@@ -10,7 +10,9 @@ import sitemap from "@astrojs/sitemap";
// https://astro.build/config
export default defineConfig({
// TODO: site: "",
// TODO: required — set the deployed site URL. Used by @astrojs/sitemap,
// canonical link tags, and absolute OG image URLs.
site: "https://www.example.com",
output: "server",
adapter: node({ mode: "standalone" }),
fonts: [

View File

@@ -1,5 +1,5 @@
{
"name": "astro-template",
"name": "site",
"type": "module",
"version": "0.0.1",
"engines": {

View File

@@ -15,8 +15,8 @@ const { href = "#main", label = "Skip to main content" } = Astro.props;
left: 0;
z-index: 100;
padding: 0.75rem 1.25rem;
/* TODO: background: var(--color-bg) */
/* TODO: color: var(--color-fg) */
background: var(--color-bg);
color: var(--color-fg);
font-size: 0.85rem;
transform: translateY(-200%);
transition: transform 150ms ease-out;
@@ -24,7 +24,7 @@ const { href = "#main", label = "Skip to main content" } = Astro.props;
.skip-link:focus-visible {
transform: translateY(0);
/* TODO: outline: 2px solid var(--color-accent); */
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
</style>

View File

@@ -2,24 +2,32 @@ import { defineCollection } from "astro:content";
import { z } from "astro/zod";
import { file } from "astro/loaders";
const addressSchema = z.object({
street: z.string(),
city: z.string(),
state: z.string(),
zip: z.string(),
full: z.string(),
});
export const siteSchema = z.object({
name: z.string(),
shortName: z.string(),
url: z.url(),
tagline: z.string(),
contactFormEndpoint: z.string(),
description: z.string(),
contactFormEndpoint: z.string().optional(),
phone: z.string(),
phoneDisplay: z.string().optional(),
phoneHref: z.string().optional(),
email: z.email(),
hours: z.string(),
address: z.object({
street: z.string(),
city: z.string(),
state: z.string(),
zip: z.string(),
full: z.string(),
}),
address: addressSchema,
offices: z.array(addressSchema.extend({ label: z.string() })).optional(),
nav: z.array(z.object({ label: z.string(), href: z.string() })),
social: z.array(z.url()).optional(),
areaServed: z.array(z.string()).optional(),
established: z.number().int().optional(),
});
const site = defineCollection({

View File

@@ -1,48 +1,42 @@
- id: main
name: "Loftin Law Group, LLC"
shortName: "Loftin Law Group"
url: "https://www.llgllc.com"
tagline: "Committed to the clients we serve."
name: "Example Business, LLC"
shortName: "Example"
url: "https://www.example.com"
tagline: "A short, memorable tagline."
description: "One-to-two sentence description of the business. Used as the default meta description and in JSON-LD."
contactFormEndpoint: "/api/contact"
description: "A Lake Charles law firm handling personal injury, criminal defense, family, real estate, successions, corporate, governmental, and litigation matters across Southwest Louisiana."
phone: "337-310-4300"
phoneDisplay: "(337) 310-4300"
phoneHref: "tel:+13373104300"
fax: "(337) 310-4400"
email: "info@llgllc.com"
hours: "Monday Friday, 8:30 AM 5:00 PM"
offices:
- label: "Lake Charles"
street: "113 Dr. Michael DeBakey Drive"
city: "Lake Charles"
state: "LA"
zip: "70601"
full: "113 Dr. Michael DeBakey Drive, Lake Charles, LA 70601"
- label: "Houston"
street: "1207 S. Shepherd Drive"
city: "Houston"
state: "TX"
zip: "77019"
full: "1207 S. Shepherd Drive, Houston, TX 77019"
phone: "555-555-5555"
phoneDisplay: "(555) 555-5555"
phoneHref: "tel:+15555555555"
email: "hello@example.com"
hours: "Mo-Fr 09:00-17:00"
address:
street: "113 Dr. Michael DeBakey Drive"
city: "Lake Charles"
state: "LA"
zip: "70601"
full: "113 Dr. Michael DeBakey Drive, Lake Charles, LA 70601"
street: "123 Main St"
city: "City"
state: "ST"
zip: "00000"
full: "123 Main St, City, ST 00000"
# Optional: list of physical offices (omit if single-location — use `address`).
# offices:
# - label: "HQ"
# street: "123 Main St"
# city: "City"
# state: "ST"
# zip: "00000"
# full: "123 Main St, City, ST 00000"
nav:
- label: "Practice Areas"
href: "/practice-areas"
- label: "Attorneys"
href: "/attorneys"
- label: "Firm"
href: "/firm-profile"
- label: "Insights"
href: "/insights"
- label: "About"
href: "/about"
- label: "Services"
href: "/services"
- label: "Contact"
href: "/contact"
affiliate:
name: "Armor Title Company, LLC"
url: "https://www.armortitle.com"
description: "Affiliated residential and commercial title and closing services for the Lake Charles area."
established: 2006
# Add as needed; consumed by JSON-LD `sameAs`.
social: []
# social:
# - "https://www.facebook.com/example"
# - "https://www.instagram.com/example"
# Optional geographic service area for LocalBusiness JSON-LD.
areaServed:
- "United States"
established: 2025

View File

@@ -14,14 +14,18 @@ interface Props {
ogImage?: string;
noindex?: boolean;
schemas?: Record<string, unknown>[];
lang?: string;
locale?: string;
}
const {
title,
description,
ogImage = "/favicon/web-app-manifest-512x512.png",
ogImage = "/og/default.png",
noindex = false,
schemas = [],
lang = "en",
locale = "en_US",
} = Astro.props;
const pageTitle = title
@@ -34,12 +38,14 @@ const allSchemas = [organizationSchema, websiteSchema, ...schemas];
---
<!doctype html>
<html lang="en">
<html lang={lang}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content={description} />
<meta name="description" content={pageDesc} />
<meta name="generator" content={Astro.generator} />
{/* TODO: set brand theme color */}
<meta name="theme-color" content="#ffffff" />
{noindex && <meta name="robots" content="noindex,nofollow" />}
<link rel="canonical" href={canonical} />
@@ -62,16 +68,16 @@ const allSchemas = [organizationSchema, websiteSchema, ...schemas];
sizes="180x180"
href="/favicon/apple-touch-icon.png"
/>
<meta name="apple-mobile-web-app-title" content={pageTitle} />
<meta name="apple-mobile-web-app-title" content={site.shortName} />
<link rel="manifest" href="/favicon/site.webmanifest" />
{/* Open Graph */}
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:title" content={pageTitle} />
<meta property="og:description" content={pageDesc} />
<meta property="og:type" content="website" />
<meta property="og:url" content={canonical} />
<meta property="og:site_name" content="Armor Title Company" />
<meta property="og:locale" content="en_US" />
<meta property="og:site_name" content={site.name} />
<meta property="og:locale" content={locale} />
<meta property="og:image" content={ogImageUrl} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />

View File

@@ -2,9 +2,21 @@ import { site } from "./site";
type Schema = Record<string, unknown>;
const postalAddress = {
"@type": "PostalAddress",
streetAddress: site.address.street,
addressLocality: site.address.city,
addressRegion: site.address.state,
postalCode: site.address.zip,
addressCountry: "US",
};
export const organizationSchema: Schema = {
"@context": "https://schema.org",
"@type": "LegalService",
// TODO: pick the most specific schema.org type for this business
// (LocalBusiness, ProfessionalService, LegalService, MedicalBusiness,
// Restaurant, Store, etc.). See https://schema.org/docs/full.html
"@type": "LocalBusiness",
"@id": `${site.url}/#organization`,
name: site.name,
alternateName: site.shortName,
@@ -12,28 +24,18 @@ export const organizationSchema: Schema = {
telephone: site.phone,
email: site.email,
description: site.description,
areaServed: [{ "@type": "State", name: "Louisiana" }],
address: {
"@type": "PostalAddress",
streetAddress: site.address.street,
addressLocality: site.address.city,
addressRegion: site.address.state,
postalCode: site.address.zip,
addressCountry: "US",
},
address: postalAddress,
location: {
"@type": "Place",
name: `${site.shortName}`,
address: {
"@type": "PostalAddress",
streetAddress: site.address.street,
addressLocality: site.address.city,
addressRegion: site.address.state,
postalCode: site.address.zip,
addressCountry: "US",
},
name: site.shortName,
address: postalAddress,
},
openingHours: site.hours,
...(site.areaServed && {
areaServed: site.areaServed.map((name) => ({ "@type": "Place", name })),
}),
...(site.social && site.social.length > 0 && { sameAs: site.social }),
...(site.established && { foundingDate: String(site.established) }),
};
export const websiteSchema: Schema = {

23
src/pages/404.astro Normal file
View File

@@ -0,0 +1,23 @@
---
import BaseLayout from "~/layouts/base-layout.astro";
---
<BaseLayout
title="Page not found"
description="The page you were looking for doesn't exist."
noindex
>
<section class="mx-auto max-w-2xl px-6 py-24 text-center">
<p class="text-sm font-medium uppercase tracking-wider text-muted">404</p>
<h1 class="mt-2 text-4xl font-bold">Page not found</h1>
<p class="mt-4 text-base text-muted">
The page you were looking for doesn't exist or has moved.
</p>
<a
href="/"
class="mt-8 inline-block rounded-md bg-accent px-5 py-2.5 text-sm font-medium text-accent-fg"
>
Back to home
</a>
</section>
</BaseLayout>

View File

@@ -1,4 +1,24 @@
@import "tailwindcss";
@theme {
/* TODO: replace with brand palette. Tailwind 4 reads these as utilities,
e.g. `--color-accent` → `bg-accent`, `text-accent`, `border-accent`. */
--color-bg: #ffffff;
--color-fg: #0a0a0a;
--color-muted: #6b7280;
--color-accent: #2563eb;
--color-accent-fg: #ffffff;
/* TODO: wire the brand font. Keep in sync with the `fonts:` block in
astro.config.mjs and the `<Font cssVariable=... />` call in base-layout. */
--font-sans:
var(--font-inter), "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
@layer base {
html {
font-family: var(--font-sans);
color: var(--color-fg);
background: var(--color-bg);
}
}