Initialized Astro template project

This commit is contained in:
2026-05-24 16:25:41 -05:00
commit 5e05a7c256
32 changed files with 10657 additions and 0 deletions

3
.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
.DS_Store
node_modules
dist

View File

@@ -0,0 +1,45 @@
name: Deploy
on:
push:
branches: ["**"]
delete:
env:
PANGOLIN_API_URL: ${{ secrets.PANGOLIN_API_URL }}
PANGOLIN_API_KEY: ${{ secrets.PANGOLIN_API_KEY }}
PANGOLIN_ORG_ID: ${{ secrets.PANGOLIN_ORG_ID }}
PANGOLIN_DOMAIN_ID: ${{ secrets.PANGOLIN_DOMAIN_ID }}
PANGOLIN_SITE_ID: ${{ secrets.PANGOLIN_SITE_ID }}
PANGOLIN_TARGET_IP: ${{ secrets.PANGOLIN_TARGET_IP }}
jobs:
deploy:
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: https://git.zyrrus.dev/eighty-six/node-deploy-action/deploy@v2
with:
app-name: ${{ vars.APP_NAME }}
branch: ${{ github.ref_name }}
internal-port: 4321
env-vars: |
MAILGUN_API_KEY=${{ secrets.MAILGUN_API_KEY }}
MAILGUN_DOMAIN=${{ secrets.MAILGUN_DOMAIN }}
CONTACT_TO=${{ secrets.CONTACT_TO }}
MAILGUN_FROM=${{ secrets.MAILGUN_FROM }}
cleanup-preview:
if: github.event_name == 'delete' && github.event.ref_type == 'branch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: main
- uses: https://git.zyrrus.dev/eighty-six/node-deploy-action/cleanup@v2
with:
app-name: ${{ vars.APP_NAME }}
branch: ${{ github.event.ref }}

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/

4
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

5
README.md Normal file
View File

@@ -0,0 +1,5 @@
# Astro Project Starter Template
```sh
npm create astro@latest -- --template https://git.zyrrus/dev/arachne/astro-template
```

43
astro.config.mjs Normal file
View File

@@ -0,0 +1,43 @@
// @ts-check
import { defineConfig, fontProviders, envField } from "astro/config";
import tailwindcss from "@tailwindcss/vite";
import preact from "@astrojs/preact";
import node from "@astrojs/node";
import sitemap from "@astrojs/sitemap";
// https://astro.build/config
export default defineConfig({
// TODO: site: "",
output: "server",
adapter: node({ mode: "standalone" }),
fonts: [
{
provider: fontProviders.google(),
name: "Inter",
cssVariable: "--font-inter",
weights: [500, 600, 700],
styles: ["normal", "italic"],
subsets: ["latin"],
display: "swap",
fallbacks: ["Segoe UI", "Roboto", "Helvetica", "Arial", "sans-serif"],
},
],
vite: {
plugins: [tailwindcss()],
},
integrations: [preact(), sitemap()],
env: {
schema: {
MAILGUN_API_KEY: envField.string({ context: "server", access: "secret" }),
MAILGUN_DOMAIN: envField.string({ context: "server", access: "secret" }),
CONTACT_TO: envField.string({ context: "server", access: "secret" }),
MAILGUN_FROM: envField.string({ context: "server", access: "secret" }),
},
},
});

15
dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM node:lts-alpine AS build
WORKDIR /app
COPY package.json ./
RUN npm i
COPY . .
RUN npm run build
FROM node:lts-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
ENV HOST=0.0.0.0
COPY --from=build /app/package.json ./
RUN npm i --omit=dev
COPY --from=build /app/dist ./dist
CMD ["node", "./dist/server/entry.mjs"]

6093
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "astro-template",
"type": "module",
"version": "0.0.1",
"engines": {
"node": ">=22.12.0"
},
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/node": "^10.1.1",
"@astrojs/preact": "^5.1.3",
"@astrojs/sitemap": "^3.7.2",
"@tailwindcss/vite": "^4.3.0",
"astro": "^6.3.7",
"preact": "^10.29.2",
"tailwindcss": "^4.3.0"
}
}

4048
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

BIN
public/favicon/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 749 B

View File

View File

View File

View File

View File

View File

@@ -0,0 +1,16 @@
---
interface Props {
schemas: Record<string, unknown>[];
}
const { schemas } = Astro.props;
---
{
schemas.map((schema) => (
<script
type="application/ld+json"
set:html={JSON.stringify(schema)}
is:inline
/>
))
}

View File

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

32
src/content.config.ts Normal file
View File

@@ -0,0 +1,32 @@
import { defineCollection } from "astro:content";
import { z } from "astro/zod";
import { file } from "astro/loaders";
export const siteSchema = z.object({
name: z.string(),
shortName: z.string(),
url: z.url(),
tagline: z.string(),
contactFormEndpoint: z.string(),
description: z.string(),
phone: z.string(),
email: z.email(),
hours: z.string(),
address: z.object({
street: z.string(),
city: z.string(),
state: z.string(),
zip: z.string(),
full: z.string(),
}),
nav: z.array(z.object({ label: z.string(), href: z.string() })),
});
const site = defineCollection({
loader: file("./src/content/site.yaml"),
schema: siteSchema,
});
export const collections = {
site,
};

48
src/content/site.yaml Normal file
View File

@@ -0,0 +1,48 @@
- id: main
name: "Loftin Law Group, LLC"
shortName: "Loftin Law Group"
url: "https://www.llgllc.com"
tagline: "Committed to the clients we serve."
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"
address:
street: "113 Dr. Michael DeBakey Drive"
city: "Lake Charles"
state: "LA"
zip: "70601"
full: "113 Dr. Michael DeBakey Drive, Lake Charles, LA 70601"
nav:
- label: "Practice Areas"
href: "/practice-areas"
- label: "Attorneys"
href: "/attorneys"
- label: "Firm"
href: "/firm-profile"
- label: "Insights"
href: "/insights"
- 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

View File

@@ -0,0 +1,97 @@
---
import "~/styles/global.css";
import { site } from "~/lib/site";
import { organizationSchema, websiteSchema } from "~/lib/schema";
import { Font } from "astro:assets";
import SeoJsonLd from "~/components/seo-json-ld.astro";
import SkipLink from "~/components/skip-link.astro";
import Header from "~/components/sections/header.astro";
import Footer from "~/components/sections/footer.astro";
interface Props {
title?: string;
description?: string;
ogImage?: string;
noindex?: boolean;
schemas?: Record<string, unknown>[];
}
const {
title,
description,
ogImage = "/favicon/web-app-manifest-512x512.png",
noindex = false,
schemas = [],
} = Astro.props;
const pageTitle = title
? `${title} | ${site.shortName}`
: `${site.name} - ${site.tagline}`;
const pageDesc = description ?? site.description;
const canonical = new URL(Astro.url.pathname, site.url).toString();
const ogImageUrl = new URL(ogImage, site.url).toString();
const allSchemas = [organizationSchema, websiteSchema, ...schemas];
---
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content={description} />
<meta name="generator" content={Astro.generator} />
{noindex && <meta name="robots" content="noindex,nofollow" />}
<link rel="canonical" href={canonical} />
<title>{pageTitle}</title>
{/* Fonts */}
<Font cssVariable="--font-inter" preload />
{/* Favicon */}
<link
rel="icon"
type="image/png"
href="/favicon/favicon-96x96.png"
sizes="96x96"
/>
<link rel="icon" type="image/svg+xml" href="/favicon/favicon.svg" />
<link rel="shortcut icon" href="/favicon/favicon.ico" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/favicon/apple-touch-icon.png"
/>
<meta name="apple-mobile-web-app-title" content={pageTitle} />
<link rel="manifest" href="/favicon/site.webmanifest" />
{/* Open Graph */}
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<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:image" content={ogImageUrl} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
{/* Twitter */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={pageTitle} />
<meta name="twitter:description" content={pageDesc} />
<meta name="twitter:image" content={ogImageUrl} />
{/* SEO */}
<SeoJsonLd schemas={allSchemas} />
<link rel="sitemap" href="/sitemap-index.xml" />
</head>
<body class="flex min-h-screen flex-col">
<SkipLink />
<Header />
<main id="main" class="flex-1">
<slot />
</main>
<Footer />
</body>
</html>

62
src/lib/schema.ts Normal file
View File

@@ -0,0 +1,62 @@
import { site } from "./site";
type Schema = Record<string, unknown>;
export const organizationSchema: Schema = {
"@context": "https://schema.org",
"@type": "LegalService",
"@id": `${site.url}/#organization`,
name: site.name,
alternateName: site.shortName,
url: site.url,
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",
},
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",
},
},
openingHours: site.hours,
};
export const websiteSchema: Schema = {
"@context": "https://schema.org",
"@type": "WebSite",
"@id": `${site.url}/#website`,
url: site.url,
name: site.name,
publisher: { "@id": `${site.url}/#organization` },
inLanguage: "en-US",
};
export function breadcrumbSchema(
crumbs: { name: string; url: string }[],
): Schema {
return {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: crumbs.map((c, i) => ({
"@type": "ListItem",
position: i + 1,
name: c.name,
item: c.url.startsWith("http") ? c.url : `${site.url}${c.url}`,
})),
};
}

10
src/lib/site.ts Normal file
View File

@@ -0,0 +1,10 @@
import { z } from "astro/zod";
import { getEntry } from "astro:content";
import { siteSchema } from "~/content.config";
const entry = await getEntry("site", "main");
if (!entry)
throw new Error("Missing site entry 'main' in src/content/site.yaml");
export type Site = z.infer<typeof siteSchema>;
export const site = entry.data as Site;

10
src/pages/index.astro Normal file
View File

@@ -0,0 +1,10 @@
---
import BaseLayout from "~/layouts/base-layout.astro";
import Hero from "~/components/sections/home/home-hero.astro";
import Cta from "~/components/sections/home/home-cta.astro";
---
<BaseLayout>
<Hero />
<Cta />
</BaseLayout>

13
src/pages/robots.txt.ts Normal file
View File

@@ -0,0 +1,13 @@
import type { APIRoute } from "astro";
const getRobotsTxt = (sitemapURL: URL) => `\
User-agent: *
Allow: /
Sitemap: ${sitemapURL.href}
`;
export const GET: APIRoute = ({ site }) => {
const sitemapURL = new URL("sitemap-index.xml", site);
return new Response(getRobotsTxt(sitemapURL));
};

4
src/styles/global.css Normal file
View File

@@ -0,0 +1,4 @@
@import "tailwindcss";
@theme {
}

12
tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"],
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact",
"paths": {
"~/*": ["./src/*"]
}
}
}