Compare commits

..

20 Commits

Author SHA1 Message Date
b4e43b0c6c Fixes header on small screens, though maybe we don't want to always hide the logo. 2025-06-20 16:16:42 -07:00
6bf7e738f8 Loads goatcounter script from www.terakoda.com/count.js 2025-06-17 16:06:02 -07:00
4936692b62 Switches back to goatcounter.terakoda.com 2025-06-17 15:59:12 -07:00
e76ac0f42c Adds referrer 2025-06-17 15:53:40 -07:00
86c1c85cb2 Moves goatcounter back onto the www domain 2025-06-17 15:50:50 -07:00
912aa15c90 Fixes goatcounter integration and adds event to the contact button 2025-06-17 15:35:02 -07:00
c795ba45fb Adds goatcounter script 2025-06-17 11:46:24 -07:00
44733d4360 Adds about page and first blog post. 2025-06-17 11:32:40 -07:00
5b8f44461b Fixes index page to be responsive for small screens 2025-06-09 16:00:07 -07:00
dea9bebdbb Removes .astro/ from tracking 2025-06-09 15:46:24 -07:00
b49b03b46f Adds benchmark settings. Removes blog and about from header 2025-06-09 15:44:42 -07:00
35fba1e43f Replaces placeholder cats with dogs 2025-06-05 13:17:44 -07:00
6994808b77 Styles the blog posts themselves 2025-06-05 13:16:01 -07:00
54b5a0b3b0 Some basic styling on the blog index 2025-06-05 13:09:19 -07:00
5431967dce Optimize images 2025-06-05 12:42:40 -07:00
fe707a600e Uses tailwind for most of the homepage styling and adds prettier 2025-06-05 12:28:41 -07:00
2f1edd9851 Adds Astro files 2025-06-04 16:38:24 -07:00
4964727717 Updates gitignore to remove .astro and .direnv 2025-06-03 12:04:51 -07:00
2ac5a7e244 Some styling and removing extra stuff 2025-06-03 12:04:11 -07:00
0c4a8a1ddf Populates the project with the blog template 2025-06-03 11:08:59 -07:00
30 changed files with 7542 additions and 0 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

5
.gitignore vendored
View File

@@ -130,3 +130,8 @@ dist
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*
# Direnv cache
.direnv/
# Astro data
.astro/

13
.prettierrc.mjs Normal file
View File

@@ -0,0 +1,13 @@
// .prettierrc.mjs
/** @type {import("prettier").Config} */
export default {
plugins: ["prettier-plugin-astro"],
overrides: [
{
files: "*.astro",
options: {
parser: "astro",
},
},
],
};

14
astro.config.mjs Normal file
View File

@@ -0,0 +1,14 @@
// @ts-check
import { defineConfig } from "astro/config";
import mdx from "@astrojs/mdx";
import sitemap from "@astrojs/sitemap";
import tailwindcss from "@tailwindcss/vite";
// https://astro.build/config
export default defineConfig({
site: "https://www.terakoda.com",
integrations: [mdx(), sitemap()],
vite: {
plugins: [tailwindcss()],
},
});

22
benchmark.yaml Normal file
View File

@@ -0,0 +1,22 @@
---
concurrency: 10
base: "https://www2.terakoda.com"
iterations: 100
rampup: 2
plan:
- name: Fetch root
request:
url: "/"
- name: Fetch css
request:
url: "/_astro/about.B804zLT3.css"
- name: Fetch logo
request:
url: "/_astro/HeaderLogo.BGOAlOcd.png"
- name: Fetch blog
request:
url: "/blog"
- name: Fetch Article
request:
url: "/blog/a-fresh-start"

42
flake.lock generated Normal file
View File

@@ -0,0 +1,42 @@
{
"nodes": {
"flake-schemas": {
"locked": {
"lastModified": 1721999734,
"narHash": "sha256-G5CxYeJVm4lcEtaO87LKzOsVnWeTcHGKbKxNamNWgOw=",
"rev": "0a5c42297d870156d9c57d8f99e476b738dcd982",
"revCount": 75,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.1.5/0190ef2f-61e0-794b-ba14-e82f225e55e6/source.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://flakehub.com/f/DeterminateSystems/flake-schemas/%2A"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1748889542,
"narHash": "sha256-Hb4iMhIbjX45GcrgOp3b8xnyli+ysRPqAgZ/LZgyT5k=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "10d7f8d34e5eb9c0f9a0485186c1ca691d2c5922",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-schemas": "flake-schemas",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

54
flake.nix Normal file
View File

@@ -0,0 +1,54 @@
# This flake was initially generated by fh, the CLI for FlakeHub (version 0.1.24)
{
# A helpful description of your flake
description = "Terakoda.com homepage";
# Flake inputs
inputs = {
flake-schemas.url = "https://flakehub.com/f/DeterminateSystems/flake-schemas/*";
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-25.05";
};
# Flake outputs that other flakes can use
outputs =
{
self,
flake-schemas,
nixpkgs,
}:
let
# Helpers for producing system-specific outputs
supportedSystems = [ "x86_64-linux" ];
forEachSupportedSystem =
f:
nixpkgs.lib.genAttrs supportedSystems (
system:
f {
pkgs = import nixpkgs { inherit system; };
}
);
in
{
# Schemas tell Nix about the structure of your flake's outputs
schemas = flake-schemas.schemas;
# Development environments
devShells = forEachSupportedSystem (
{ pkgs }:
{
default = pkgs.mkShell {
packages = with pkgs; [
git # Version control
nodejs_22 # Base language utils
eslint # Linting
astro-language-server # LSP
nodePackages.prettier # Formatting
nixpkgs-fmt # Formatting
drill # Benchmarking
];
};
}
);
};
}

6703
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "terakoda.com",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/mdx": "^4.3.0",
"@astrojs/rss": "^4.0.11",
"@astrojs/sitemap": "^3.4.0",
"@tailwindcss/vite": "^4.1.8",
"astro": "^5.8.1",
"less": "^4.3.0",
"tailwindcss": "^4.1.8"
},
"devDependencies": {
"@astrojs/ts-plugin": "^1.10.4",
"prettier": "^3.5.3",
"prettier-plugin-astro": "^0.14.1"
}
}

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Binary file not shown.

BIN
src/assets/HeaderLogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,55 @@
---
// Import the global.css file here so that it is included on
// all pages through the use of the <BaseHead /> component.
import '../styles/global.css';
import { SITE_TITLE } from '../consts';
interface Props {
title: string;
description: string;
image?: string;
}
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { title, description, image = '/blog-placeholder-1.jpg' } = Astro.props;
---
<!-- Global Metadata -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
<link rel="sitemap" href="/sitemap-index.xml" />
<link
rel="alternate"
type="application/rss+xml"
title={SITE_TITLE}
href={new URL('rss.xml', Astro.site)}
/>
<meta name="generator" content={Astro.generator} />
<!-- Font preloads -->
<link rel="preload" href="/fonts/atkinson-regular.woff" as="font" type="font/woff" crossorigin />
<link rel="preload" href="/fonts/atkinson-bold.woff" as="font" type="font/woff" crossorigin />
<!-- Canonical URL -->
<link rel="canonical" href={canonicalURL} />
<!-- Primary Meta Tags -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content={Astro.url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(image, Astro.url)} />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={Astro.url} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={new URL(image, Astro.url)} />

View File

@@ -0,0 +1,19 @@
---
interface Props {
href: string;
event?: string;
eventTitle?: string;
}
const { href, event, eventTitle } = Astro.props;
---
<a
href={href}
data-goatcounter-click={event}
data-goatcounter-title={eventTitle}
data-goatcounter-referrer="www.terakoda.com"
class="text-dark bg-primary rounded-md px-8 py-4 font-bold hover:bg-primary-dark"
>
<slot />
</a>

View File

@@ -0,0 +1,13 @@
---
const today = new Date();
---
<footer
class="py-4 text-sm text-center bg-dark text-light absolute bottom-0 w-full"
>
&copy; {today.getFullYear()} Terakoda, LLC. All rights reserved.
<script
data-goatcounter="https://goatcounter.terakoda.com/count"
async
src="https://www.terakoda.com/count.js"></script>
</footer>

View File

@@ -0,0 +1,17 @@
---
interface Props {
date: Date;
}
const { date } = Astro.props;
---
<time datetime={date.toISOString()} class="text-medium">
{
date.toLocaleDateString("en-us", {
year: "numeric",
month: "short",
day: "numeric",
})
}
</time>

View File

@@ -0,0 +1,49 @@
---
import HeaderLink from "./HeaderLink.astro";
import HeaderLogo from "../assets/HeaderLogo.png";
---
<header>
<nav class="flex-col md:flex-row">
<div class="hidden md:block">
<a href="/">
<img
src={HeaderLogo.src}
alt="Terakoda"
height={HeaderLogo.height / 4}
width={HeaderLogo.width / 4}
/>
</a>
</div>
<div class="internal-links">
<HeaderLink href="/">Home</HeaderLink>
<HeaderLink href="/blog">Blog</HeaderLink>
<HeaderLink href="/about">About</HeaderLink>
</div>
</nav>
</header>
<style>
header {
margin: 0;
padding: 0 1em;
box-shadow: 0 2px 8px rgba(var(--black), 5%);
background-color: #1f2932; /* Dark Background */
color: #fdfafc; /* Light Text */
}
nav {
display: flex;
align-items: center;
justify-content: space-between;
}
nav a {
padding: 1em 0.5em;
color: var(--black);
border-bottom: 4px solid transparent;
text-decoration: none;
}
nav a.active {
text-decoration: none;
border-bottom-color: var(--color-primary);
}
</style>

View File

@@ -0,0 +1,24 @@
---
import type { HTMLAttributes } from 'astro/types';
type Props = HTMLAttributes<'a'>;
const { href, class: className, ...props } = Astro.props;
const pathname = Astro.url.pathname.replace(import.meta.env.BASE_URL, '');
const subpath = pathname.match(/[^\/]+/g);
const isActive = href === pathname || href === '/' + (subpath?.[0] || '');
---
<a href={href} class:list={[className, { active: isActive }]} {...props}>
<slot />
</a>
<style>
a {
display: inline-block;
text-decoration: none;
}
a.active {
font-weight: bolder;
text-decoration: underline;
}
</style>

2
src/consts.ts Normal file
View File

@@ -0,0 +1,2 @@
export const SITE_TITLE = "Terakoda";
export const SITE_DESCRIPTION = "A tech cooperative";

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

@@ -0,0 +1,19 @@
import { glob } from "astro/loaders";
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
// Load Markdown and MDX files in the `src/content/blog/` directory.
loader: glob({ base: "./src/content/blog", pattern: "**/*.{md,mdx}" }),
// Type-check frontmatter using a schema
schema: z.object({
title: z.string(),
description: z.string(),
// Transform string to Date object
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
author: z.string().optional(),
}),
});
export const collections = { blog };

View File

@@ -0,0 +1,54 @@
---
title: "A Fresh Start: Why we are starting a Terakoda"
description: "Why we decided to quit our tech jobs and start Terakoda as a cooperative."
pubDate: "2025-06-17"
author: Drew Haven
---
I quit my job in March. It wasnt an easy decision. It qualified as a Good Job on basically every metric: good pay, growing business, interesting technical problems, smart co-workers. There was very little to criticize about the company. However, I realized it wasnt the right fit for me. Its the culmination of some ideas and feelings that have been growing and solidifying over the last six years and four companies. I realized I needed to try something different.
I talked it over with my wife and we decided that we would try this together. On top of that, we decided that what we really wanted was to be both independent and part of a cooperative. That might seem like a contradiction, but let me explain.
## First, a little history
Ive been working in and around Silicon Valley for two decades now. My first job was plugging in cables at the San Jose Convention Center in 2005. Between then and now Ive done many things. I worked at big companies like Google. I worked at start-ups, so many start-ups, small start-ups, dying start-ups, zombie start-ups, even founding a short-lived start-up. I was lucky enough to be around for one IPO. Ive also done freelance work a few times, though never for long.
Lindsay worked in the video game industry for a decade. She did design and production. She was a producer when THQ shut her studio down. She did production at a few small studios trying to get in on the mobile-game gold-rush. Eventually she landed at Cryptic and did design and production on a major Dungeons & Dragons game launch: Neverwinter. Ultimately, she left that industry to go back to school looking for a change.
In short, weve had enviable careers, but find ourselves disillusioned with the industry. Our careers have gone well, but something just hasnt felt right.
## Congruence
A key issue that weve faced is that the values we hold and those of the companies we work for dont align. We believed in a set of values, but found our professional lives following a different set. Living with this cognitive dissonance was putting a stress on the both of us. Weve decided that its time to pursue a career that better matches those values.
What are some of those values? Here are just a few that weve identified.
We believe in self-determination. Many workplaces dont give their employees much agency. Your ability to succeed, grow or advance may be largely out of your control. Instead its determined in the majority by the projects or managers you are assigned to. Many people dont have the ability to choose what to work on. Even if those choices would ultimately come to the same conclusion they arent allowed to make that decision themselves. And dont even worry about realizing theres something more important to do if its not part of your managers roadmap.
Similarly, we believe in autonomy. This is the right of everyone to work in the way that works best for them. This is a fundamental support for equity.
A system of cooperation will always produce better outcomes than a system of competition. Competition is fundamentally a structure of destruction. Its about beating your opponent, about taking away their resources, about taking them down. It leads to a system where there is less. Cooperation is about building each other up, about sharing resources and information. Cooperation creates and adds to the system.
But, wait, what about all the benefits of competitions? Are you saying sports are negative? No. I frame those as cooperative endeavors. They are inherently governed by a set of rules that the participants put in place so that they can create a space where they can motivate each person to do their best. Compare that to business where the goal of most businesses is to take customers and resources from competitors. Its framed in terms of selfishly taking and hording as much as possible. How many times have you seen the founder of a failed business go out and congratulate their competitor? How often do they publicly say that they appreciate the financial loss because of the lessons it taught them?
We also believe that every person in an organization is valuable and worthy of respect. Many tech companies recently have begun laying off workers and doing their best to replace them with AI. We believe that every worker deserves a voice in their workplace.
This also extends to the customers! Customers are people too. The worst phase we ever heard in our professional lives was when someone said, unironically, “its our money in their pockets”. Companies should exist to serve the customers and the employees just as much as the shareholders.
## So what now?
We could try to opt out of the system. We could go join a commune in the mountains, grow our own carrots and make our own goat cheese. Thats one alternative, but its not an attractive one. For one, it doesnt leverage any of the skills that we have developed during our careers. But more than that, it cuts us off from the very industry that we would like to see change. So that is why we are going to a new start-up, but we arent going to do things the way weve seen them done.
We are founding a new tech cooperative. We will be building a company that follows the principles of cooperatives. We will be democratic, we will be sustainable, we will work for the benefit for our workers, our customers, our communities and the planet.
We dont know exactly what our business model is going to be. This is a start-up, and a start-up is all about the search for a new business model. We are going to embark on the customer development journey. Most of the text on the topic is framed in terms of high-scale, VC-backed companies, but many of the points work just as well for a small company.
We will be small for some time. We dont know how large we will grow, but we will grow when it makes sense to do so based on our business model. We will need to balance the goals of creating a profitable, sustainable company with the goal of creating a cooperative alternative to Silicon Valley competition.
Weve named ourselves Terakoda. Tera- could reference the Greek-origin SI prefix for one trillion, evoking the potential scale of technology. It could also reference Latin word “terra” meaning land or earth. Koda might be reminiscent of “code”, again referencing technology. But also coda, which is a passage that brings a piece to conclusion, much as we hope this represents the conclusion of the search for a fulfilling career.
## A journey begins
And so the next phase of our careers begins. We dont know quite where it will take us, but we know what direction we are moving in. We are excited to embrace a community of respect, cooperation, resilience and sustainability. We will do our best to live our values every day.
This wont be a journey we undertake alone. We will rely on the help of many others who have walked these paths before us. We invite you to participate. Please follow along. Dont hesitate to reach out if you have any thoughts, advice or opportunities for us. We welcome the cooperation.

View File

@@ -0,0 +1,97 @@
---
import type { CollectionEntry } from "astro:content";
import BaseHead from "../components/BaseHead.astro";
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import FormattedDate from "../components/FormattedDate.astro";
type Props = CollectionEntry<"blog">["data"];
const { title, description, pubDate, updatedDate, heroImage, author } =
Astro.props;
---
<html lang="en">
<head>
<BaseHead title={title} description={description} />
<style>
main {
width: calc(100% - 2em);
max-width: 100%;
margin: 0;
}
.hero-image {
width: 100%;
}
.hero-image img {
display: block;
margin: 0 auto;
border-radius: 12px;
box-shadow: var(--box-shadow);
}
.prose {
width: 720px;
max-width: calc(100% - 2em);
margin: auto;
padding: 1em;
color: rgb(var(--gray-dark));
}
.title {
margin-bottom: 1em;
padding: 1em 0;
text-align: center;
line-height: 1;
}
.title h1 {
margin: 0 0 0.5em 0;
}
.last-updated-on {
font-style: italic;
}
</style>
<!-- Set style as global so that it can apply to the rendered elements -->
<style is:global>
@reference "tailwindcss";
.content h2 {
font-size: 1.5em;
}
.content p {
margin-bottom: 1em;
line-height: 1.5em;
}
</style>
</head>
<body class="relative min-h-dvh">
<Header />
<main>
<article class="max-w-200 m-auto flex flex-col items-center py-16">
{
heroImage && heroImage.trim().length > 0 && (
<div class="">
<img src={heroImage} alt="" class="max-h-100" />
</div>
)
}
<div class="flex flex-col gap-2 items-center m-4">
<h1 class="text-3xl">{title}</h1>
{author && <p class="author text-sm text-medium">{author}</p>}
<div class="published-on text-sm text-medium">
<FormattedDate date={pubDate} />
</div>
{
updatedDate && (
<div class="last-updated-on text-sm text-medium">
Last updated on <FormattedDate date={updatedDate} />
</div>
)
}
</div>
<div class="content">
<slot />
</div>
</article>
</main>
<Footer />
</body>
</html>

54
src/pages/about.astro Normal file
View File

@@ -0,0 +1,54 @@
---
import Layout from "../layouts/BlogPost.astro";
---
<Layout
title="About Terakoda"
description="About Terakoda"
pubDate={new Date("June 9, 2025")}
heroImage=""
>
<p>
Terakoda is a software cooperative. We commit to the <a
href="https://ica.coop/en/cooperatives/cooperative-identity/"
>Seven Cooperative Principles</a
>.
</p>
<div class="members">
<h2>Members</h2>
<div class="member">
<h3 class="member-name">Drew Haven</h3>
<p>
Drew has two decades of experience working in technology, from large
multi-nationals to small start-ups and freelance work. He specializes in
web systems, software architecture and sustainable development.
</p>
</div>
<div class="member">
<h3 class="member-name">Lindsay Haven</h3>
<p>
Lindsay spent a decade in the video game industry bringing joy to users
through projects from large-budget MMOs and small mobile apps. She
specializes in organization and communication within teams.
</p>
</div>
</div>
<style lang="less">
@reference "tailwindcss";
h2 {
@apply text-3xl text-center;
}
.members {
@apply my-4;
.member {
@apply pb-4;
&-name {
@apply text-xl;
color: var(--color-accent);
}
}
}
</style>
</Layout>

View File

@@ -0,0 +1,21 @@
---
import { type CollectionEntry, getCollection } from 'astro:content';
import BlogPost from '../../layouts/BlogPost.astro';
import { render } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post) => ({
params: { slug: post.id },
props: post,
}));
}
type Props = CollectionEntry<'blog'>;
const post = Astro.props;
const { Content } = await render(post);
---
<BlogPost {...post.data}>
<Content />
</BlogPost>

View File

@@ -0,0 +1,57 @@
---
import BaseHead from "../../components/BaseHead.astro";
import Header from "../../components/Header.astro";
import Footer from "../../components/Footer.astro";
import { SITE_TITLE, SITE_DESCRIPTION } from "../../consts";
import { getCollection } from "astro:content";
import FormattedDate from "../../components/FormattedDate.astro";
const posts = (await getCollection("blog")).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
---
<!doctype html>
<html lang="en">
<head>
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
</head>
<body class="relative min-h-dvh">
<Header />
<main>
<section class="max-w-200 m-auto my-16">
<ul>
{
posts.map((post) => (
<li class="border-t-2 border-medium p-4 first:border-0">
<a href={`/blog/${post.id}/`}>
<div class="flex flex-row gap-4">
{post.data.heroImage &&
post.data.heroImage.trim().length > 0 && (
<div class="flex-none">
<img
width={200}
height={200}
src={post.data.heroImage}
alt=""
/>
</div>
)}
<div>
<p class="text-medium">
<FormattedDate date={post.data.pubDate} />
</p>
<h4 class="text-xl font-bold">{post.data.title}</h4>
<p>{post.data.description}</p>
</div>
</div>
</a>
</li>
))
}
</ul>
</section>
</main>
<Footer />
</body>
</html>

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

@@ -0,0 +1,133 @@
---
import BaseHead from "../components/BaseHead.astro";
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import ButtonLink from "../components/ButtonLink.astro";
import { SITE_TITLE, SITE_DESCRIPTION } from "../consts";
import HeaderLogo from "../assets/HeaderLogo.png";
---
<!doctype html>
<html lang="en">
<head>
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
</head>
<body class="relative min-h-dvh">
<Header />
<div class="bg-dark text-light py-16">
<div class="flex flex-col items-center gap-8 max-w-200 m-auto px-4">
<img src={HeaderLogo.src} alt="Terakoda" class="max-w-full" />
<p>
Solving Your Unique Software Challenges with Precision and Quality
</p>
<ButtonLink href="#contact">Get a Consultation</ButtonLink>
</div>
</div>
<main>
<div class="bg-light-accent py-16 text-dark">
<div class="max-w-200 m-auto px-4">
<h2 class="text-4xl mb-8 text-center">Our Expertise</h2>
<div class="grid md:grid-cols-2 grid-cols-1 gap-8">
<div class="service-item">
<h3>Custom Software Solutions</h3>
<p>
We specialize in providing bespoke software solutions for those
critical, isolated challenges that can impact your operations
and growth.
</p>
</div>
<div class="service-item">
<h3>Engineering Challenges</h3>
<p>
Our experienced developers can tackle complex engineering
hurdles that fall outside your team's immediate expertise.
</p>
</div>
<div class="service-item">
<h3>Organizational Challenges</h3>
<p>
We offer solutions to organizational challenges that can be
addressed through tailored software applications.
</p>
</div>
</div>
</div>
</div>
<div class="py-16 max-w-200 m-auto px-4">
<h2 class="text-4xl mb-8 text-center">Our Commitment to Quality</h2>
<div class="quality-content">
<p>
At Terakoda Software Systems, we are dedicated to delivering
solutions that meet the highest standards of quality and
reliability.
</p>
<ul class="quality-list">
<li>Expert Analysis to understand your specific needs.</li>
<li>Custom-Crafted Solutions designed for optimal performance.</li>
<li>
Uncompromising Quality through rigorous development standards.
</li>
<li>Clear Communication throughout the entire process.</li>
<li>Long-Term Value ensuring lasting solutions.</li>
</ul>
</div>
</div>
<div class="py-16 bg-dark text-light" id="contact">
<div
class="max-w-200 m-auto px-4 flex flex-col items-center gap-8 text-center"
>
<h2 class="text-4xl text-center">
Ready to Solve Your Software Puzzle?
</h2>
<p class="text-lg">
Contact us today for a consultation and let Terakoda Software
Systems help you overcome your unique software challenges with
precision and excellence.
</p>
<ButtonLink
href="mailto:contact@terakoda.com"
event="contact-us"
eventTitle="Contact Us"
>
Contact Us
</ButtonLink>
</div>
</div>
</main>
<Footer />
</body><style>
.service-item {
background-color: white;
padding: 25px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.service-item h3 {
color: #2eb670; /* Accent Color */
margin-top: 0;
margin-bottom: 10px;
}
.quality-list {
list-style: none;
padding-left: 0;
}
.quality-list li {
margin-bottom: 15px;
padding-left: 25px;
position: relative;
}
.quality-list li::before {
content: "\2713"; /* Checkmark */
color: #2eb670; /* Accent Color */
position: absolute;
left: 0;
}
</style>
</html>

16
src/pages/rss.xml.js Normal file
View File

@@ -0,0 +1,16 @@
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
export async function GET(context) {
const posts = await getCollection('blog');
return rss({
title: SITE_TITLE,
description: SITE_DESCRIPTION,
site: context.site,
items: posts.map((post) => ({
...post.data,
link: `/blog/${post.id}/`,
})),
});
}

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

@@ -0,0 +1,25 @@
@import "tailwindcss";
:root {
--accent: #2eb670; /* Accent Color */
--accent-dark: rgb(23, 91, 56);
--black: 15, 18, 25;
--gray: 96, 115, 159;
--gray-light: 229, 233, 240;
--gray-dark: 34, 41, 57;
--gray-gradient: rgba(var(--gray-light), 50%), #fff;
--box-shadow:
0 2px 6px rgba(var(--gray), 25%), 0 8px 24px rgba(var(--gray), 33%),
0 16px 32px rgba(var(--gray), 33%);
}
@theme {
--color-primary: rgb(46, 182, 112);
--color-primary-dark: #269e61;
--color-light: #fdfafc;
/* Mix light and dark using the perceptually uniform oklab color space */
--color-medium: color-mix(in oklab, var(--color-light), var(--color-dark));
--color-dark: #1f2932;
--color-light-accent: #f9f7f7; /* Slightly darker light background for contrast */
--color-accent: #2eb670; /* Accent Color */
}

8
tsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"],
"compilerOptions": {
"strictNullChecks": true
}
}