Fixed various issues
This commit is contained in:
5
.env.example
Normal file
5
.env.example
Normal 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
181
README.md
@@ -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
|
||||
```
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "astro-template",
|
||||
"name": "site",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"engines": {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
23
src/pages/404.astro
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user