Added CDK

main
Paco Hope 2023-12-14 09:13:18 -05:00
parent 04cd25a843
commit d306046707
15 changed files with 800 additions and 0 deletions

11
cdk/LICENSE Normal file
View File

@ -0,0 +1,11 @@
Copyright (c) <year> <owner>.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

102
cdk/README.md Normal file
View File

@ -0,0 +1,102 @@
# Static Site with AWS CDK
This sample project uses S3, CloudFront and Route53 to create a stack for
hosting static web sites.
![Architecture](static-site.png)
## Prerequisites
- A registered domain in Route53.
- If using a public S3 bucket, a secret phrase needs to be created in the parameter store. The name of the parameter to be passed in the context as `origin_custom_header_parameter_name`. Make sure to create the parameter in the same region as the static site. This parameter will be set in both CloudFront and S3 Bucket Policy to make sure only traffic from CloudFront can access the S3 objects.
- If using an existing certificate, make sure it's in the `us-east-1` region otherwise the deployment fails. [See this link](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cnames-and-https-requirements.html#https-requirements-aws-region)
## Install, Build, Deploy and Clean up
### CDK
Install AWS CDK Toolkit
```sh
npm install -g aws-cdk
```
Verify the installation
```sh
cdk --version
```
Bootstrap the AWS environment
```
cdk bootstrap aws://123456789012/us-east-1
```
### Python Setup
Create a python virtual environment in the root directory of the example.
```sh
python3 -m venv .env
```
Activate the virtual environment and install the dependencies.
```sh
source .env/bin/activate
pip install -r requirements
```
Synthesize the CloudFromation template.
```sh
cdk synth
```
Deploy the stack.
> Make sure your aws profile has sufficient permissions to create the resources. Update the `cdk.json` according to your settings. See the [Context Values](#context-values) down below.
```sh
cdk deploy
```
Clean up and remove the stack.
```sh
cdk destroy
```
## Context Values
Context values are key-value pairs that can be provided to the cdk app. It can be done in [multiple ways](https://docs.aws.amazon.com/cdk/latest/guide/context.html) such as `--context` option to the `cdk` command, or `cdk.json` file.
- **namespace**: Use as a prefix for the resource names
- **domain_name**: domain name. e.g. example.com
- **sub_domain_name**: If provided, the site will be hosted under the sub domain name (e.g. blog.example.com)
- **enable_s3_website_endpoint**: If `true` it creates a public S3 bucket with website endpoint enabled. Otherwise it creates a private S3 bucket.
- **origin_custom_header_parameter_name**: In case of using a public S3 bucket with website enabled, we can use a custom header (e.g. referer) to block all traffic S3 except from the CloudFront. This parameter is the reference to the parameter in *parameter store* where we keep the secret phrase.
- **domain_certificate_arn**: If provided, CloudFront uses this certificate for the domain. Otherwise, it creates a new certificate.
- **hosted_zone_id**: Route53 Hosted Zone ID for the domain
- **hosted_zone_name**: Route53 Hosted Zone Name for the domain
### cdk.json examples
A site with a public S3 bucket. Bucket policy limits the access to s3 objects by using `referer` header.
```json
...
"namespace": "static-site",
"domain_name": "example.com",
"enable_s3_website_endpoint": true,
"origin_custom_header_parameter_name": "/prod/static-site/origin-custom-header/referer",
"hosted_zone_id": "ZABCDE12345",
"hosted_zone_name": "example.com."
...
```
Above example with a subdomain setup. It hosts the site under `blog.example.com`
```json
...
"namespace": "static-site",
"domain_name": "example.com",
"sub_domain_name": "blog",
"enable_s3_website_endpoint": true,
"origin_custom_header_parameter_name": "/prod/static-site/origin-custom-header/referer",
"hosted_zone_id": "ZABCDE12345",
"hosted_zone_name": "example.com."
...
```
A site with a private S3 bucket origin.
```json
...
"namespace": "static-site",
"domain_name": "example.com",
"hosted_zone_id": "ZABCDE12345",
"hosted_zone_name": "example.com."
...
```

39
cdk/app.py Normal file
View File

@ -0,0 +1,39 @@
#!/usr/bin/env python3
import os
from aws_cdk import App, Environment
from site_stack import StaticSiteStack
app = App()
props = {
"namespace": app.node.try_get_context("namespace"),
"domain_name": app.node.try_get_context("domain_name"),
"sub_domain_name": app.node.try_get_context("sub_domain_name"),
"domain_certificate_arn": app.node.try_get_context(
"domain_certificate_arn"
),
"enable_s3_website_endpoint": app.node.try_get_context(
"enable_s3_website_endpoint"
),
"origin_custom_header_parameter_name": app.node.try_get_context(
"origin_custom_header_parameter_name"
),
"hosted_zone_id": app.node.try_get_context("hosted_zone_id"),
"hosted_zone_name": app.node.try_get_context("hosted_zone_name"),
}
env = Environment(
account="096320175891",
region="us-east-2"
)
StaticSite = StaticSiteStack(
scope=app,
construct_id=f"{props['namespace']}-stack",
props=props,
env=env,
description="John Mastodon",
)
app.synth()

10
cdk/cdk.json Normal file
View File

@ -0,0 +1,10 @@
{
"app": "python app.py",
"context": {
"namespace": "jmasto",
"domain_name": "johnmastodon.me",
"origin_custom_header_parameter_name": "",
"hosted_zone_id": "Z08668322WPXN73U9X1Q4",
"hosted_zone_name": "johnmastodon.me."
}
}

3
cdk/requirements.txt Normal file
View File

@ -0,0 +1,3 @@
aws-cdk-lib
constructs
pytest

55
cdk/site_stack.py Normal file
View File

@ -0,0 +1,55 @@
from aws_cdk import CfnOutput, Stack
from static_site import StaticSitePublicS3, StaticSitePrivateS3
class StaticSiteStack(Stack):
def __init__(self, scope, construct_id, props, **kwargs):
super().__init__(scope, construct_id, **kwargs)
site_domain_name = props["domain_name"]
if props["sub_domain_name"]:
site_domain_name = (
f'{props["sub_domain_name"]}.{props["domain_name"]}'
)
# If S3 website endpoint enabled, it creates the static site using a
# public S3 as the origin. Otherwise, it creates a private S3 as the
# origin.
if props["enable_s3_website_endpoint"]:
site = StaticSitePublicS3(
self,
f"{props['namespace']}-construct",
site_domain_name=site_domain_name,
domain_certificate_arn=props["domain_certificate_arn"],
origin_referer_header_parameter_name=props[
"origin_custom_header_parameter_name"
],
hosted_zone_id=props["hosted_zone_id"],
hosted_zone_name=props["hosted_zone_name"],
)
else:
site = StaticSitePrivateS3(
self,
f"{props['namespace']}-construct",
site_domain_name=site_domain_name,
domain_certificate_arn=props["domain_certificate_arn"],
hosted_zone_id=props["hosted_zone_id"],
hosted_zone_name=props["hosted_zone_name"],
)
# Add stack outputs
CfnOutput(
self,
"SiteBucketName",
value=site.bucket.bucket_name,
)
CfnOutput(
self,
"DistributionId",
value=site.distribution.distribution_id,
)
CfnOutput(
self,
"CertificateArn",
value=site.certificate.certificate_arn,
)

229
cdk/static_site.py Normal file
View File

@ -0,0 +1,229 @@
"""
Two constructs to host static sites in aws using S3, cloudfront and Route53.
StaticSitePrivateS3 creates a private S3 bucket and uses S3 API endpoint as
an origin in cloudfront and Origin Access Identity (OAI) to access the s3 objects.
StaticSitePublicS3 creates a public S3 bucket with website enabled and
uses Origin Custom Header (referer) to limit the access of s3 objects to the
CloudFront only.
"""
from aws_cdk import (
aws_s3 as s3,
aws_cloudfront as cloudfront,
aws_cloudfront_origins as origins,
aws_certificatemanager as acm,
aws_route53 as route53,
aws_route53_targets as targets,
aws_iam as iam,
aws_ssm as ssm,
RemovalPolicy
)
from constructs import Construct
class StaticSite(Construct):
"""The base class for StaticSite constructs"""
def __init__(
self,
scope,
construct_id,
site_domain_name,
hosted_zone_id,
hosted_zone_name,
domain_certificate_arn=None,
**kwargs,
):
super().__init__(scope, construct_id, **kwargs)
# Public variables
self.bucket = None
self.certificate = None
self.distribution = None
# Internal variables
self._site_domain_name = site_domain_name
# Instance Variables
self.__domain_certificate_arn = domain_certificate_arn
self.__hosted_zone_id = hosted_zone_id
self.__hosted_zone_name = hosted_zone_name
def _build_site(self):
"""The Template Method for building the site.
It uses hook functions which are implemented in the sub classes
"""
# Create the S3 bucket for the site contents
self._create_site_bucket()
# Get the hosted zone based on the provided domain name
hosted_zone = self.__get_hosted_zone()
# Get an existing or create a new certificate for the site domain
self.__create_certificate(hosted_zone)
# create the cloud front distribution
self._create_cloudfront_distribution()
# Create a Route53 record
self.__create_route53_record(hosted_zone)
def _create_site_bucket(self):
"""a virtual function to be implemented by the sub classes"""
def _create_cloudfront_distribution(self):
"""a virtual function to be implemented by the sub classes"""
def __get_hosted_zone(self):
return route53.HostedZone.from_hosted_zone_attributes(
self,
"hosted_zone",
zone_name=self.__hosted_zone_name,
hosted_zone_id=self.__hosted_zone_id,
)
def __create_route53_record(self, hosted_zone):
route53.ARecord(
self,
"site-alias-record",
record_name=self._site_domain_name,
zone=hosted_zone,
target=route53.RecordTarget.from_alias(
targets.CloudFrontTarget(self.distribution)
),
)
def __create_certificate(self, hosted_zone):
if self.__domain_certificate_arn:
# If certificate arn is provided, import the certificate
self.certificate = acm.Certificate.from_certificate_arn(
self,
"site_certificate",
certificate_arn=self.__domain_certificate_arn,
)
else:
# If certificate arn is not provided, create a new one.
# ACM certificates that are used with CloudFront must be in
# the us-east-1 region.
self.certificate = acm.DnsValidatedCertificate(
self,
"site_certificate",
domain_name=self._site_domain_name,
hosted_zone=hosted_zone,
region="us-east-1",
)
class StaticSitePrivateS3(StaticSite):
def __init__(
self,
scope,
construct_id,
**kwargs,
):
super().__init__(scope, construct_id, **kwargs)
self._build_site()
def _create_site_bucket(self):
"""Creates a private S3 bucket for the static site construct"""
self.bucket = s3.Bucket(
self,
"site_bucket",
bucket_name=self._site_domain_name,
encryption=s3.BucketEncryption.S3_MANAGED,
block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
removal_policy=RemovalPolicy.DESTROY,
auto_delete_objects=True,
)
def _create_cloudfront_distribution(self):
"""Create a cloudfront distribution with a private bucket as the origin"""
self.distribution = cloudfront.Distribution(
self,
"cloudfront_distribution",
default_behavior=cloudfront.BehaviorOptions(
origin=origins.S3Origin(self.bucket),
viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
),
domain_names=[self._site_domain_name],
certificate=self.certificate,
default_root_object="index.html",
)
class StaticSitePublicS3(StaticSite):
def __init__(
self,
scope,
construct_id,
origin_referer_header_parameter_name,
**kwargs,
):
super().__init__(scope, construct_id, **kwargs)
# Get the origin referer header value
self.__origin_referer_header = self.__get_referer_header(
origin_referer_header_parameter_name,
)
self._build_site()
def __get_referer_header(self, parameter_name):
return ssm.StringParameter.from_string_parameter_attributes(
self, "custom_header", parameter_name=parameter_name
).string_value
def _create_site_bucket(self):
"""Creates a public S3 bucket for the static site construct"""
self.bucket = s3.Bucket(
self,
"site_bucket",
bucket_name=self._site_domain_name,
website_index_document="index.html",
website_error_document="404.html",
removal_policy=RemovalPolicy.DESTROY,
auto_delete_objects=True,
)
bucket_policy = iam.PolicyStatement(
actions=["s3:GetObject"],
resources=[self.bucket.arn_for_objects("*")],
principals=[iam.AnyPrincipal()],
)
bucket_policy.add_condition(
"StringEquals",
{"aws:Referer": self.__origin_referer_header},
)
self.bucket.add_to_resource_policy(bucket_policy)
def _create_cloudfront_distribution(self):
"""Create a cloudfront distribution with a public bucket as the origin"""
origin_source = cloudfront.CustomOriginConfig(
domain_name=self.bucket.bucket_website_domain_name,
origin_protocol_policy=cloudfront.OriginProtocolPolicy.HTTP_ONLY,
origin_headers={"Referer": self.__origin_referer_header},
)
self.distribution = cloudfront.CloudFrontWebDistribution(
self,
"cloudfront_distribution",
viewer_certificate = cloudfront.ViewerCertificate.from_acm_certificate(self.certificate,
aliases=[self._site_domain_name],
security_policy=cloudfront.SecurityPolicyProtocol.TLS_V1_2_2019,
ssl_method=cloudfront.SSLMethod.SNI
),
origin_configs=[
cloudfront.SourceConfiguration(
custom_origin_source=origin_source,
behaviors=[
cloudfront.Behavior(
is_default_behavior=True,
)
],
)
],
viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
price_class=cloudfront.PriceClass.PRICE_CLASS_ALL,
)

0
cdk/tests/__init__.py Normal file
View File

61
cdk/tests/test_stacks.py Normal file
View File

@ -0,0 +1,61 @@
import pytest
from aws_cdk import App
from site_stack import StaticSiteStack
@pytest.fixture(scope="session")
def synth():
app = App(
context={
"namespace": "static-site",
"domain_name": "example.com",
"domain_certificate_arn": "arn:aws:acm:us-east-1:123456789012:certificate/abc",
"sub_domain_name": "blog",
"origin_custom_header_parameter_name": "/prod/static-site/referer",
"hosted_zone_id": "ZABCEF12345",
"hosted_zone_name": "example.com.",
}
)
props = {
"namespace": app.node.try_get_context("namespace"),
"domain_name": app.node.try_get_context("domain_name"),
"sub_domain_name": app.node.try_get_context("sub_domain_name"),
"domain_certificate_arn": app.node.try_get_context(
"domain_certificate_arn"
),
"enable_s3_website_endpoint": app.node.try_get_context(
"enable_s3_website_endpoint"
),
"origin_custom_header_parameter_name": app.node.try_get_context(
"origin_custom_header_parameter_name"
),
"hosted_zone_id": app.node.try_get_context("hosted_zone_id"),
"hosted_zone_name": app.node.try_get_context("hosted_zone_name"),
}
StaticSiteStack(
scope=app,
construct_id=props["namespace"],
props=props,
env={"account": "123456789012", "region": "us-east-1"},
)
return app.synth()
def get_buckets(stack):
return [
v
for k, v in stack.template["Resources"].items()
if v["Type"] == "AWS::S3::Bucket"
]
def test_created_stacks(synth):
assert {"static-site"} == {x.id for x in synth.stacks}
def test_site_bucket(synth):
stack = [x for x in synth.stacks if x.id == "static-site"][0]
buckets = get_buckets(stack)
assert buckets[0]["Properties"]["BucketName"] == "blog.example.com"

View File

@ -0,0 +1,78 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta http-equiv="Accept-CH" content="DPR, Viewport-Width, Width">
<link rel="icon" href={{ .Site.Params.favicon | default "/fav.png" }} type="image/gif">
<!-- font configuration -->
<link rel="stylesheet" href="{{ .Site.Params.staticPath }}/css/font.css" media="all">
<link rel="preload" as="style" href="/css/alata1.css">
<link rel="stylesheet" href="/css/alata1.css" media="print" onload="this.media='all'" />
<noscript><link href="/css/alata1.css" rel="stylesheet"></noscript>
<link rel="stylesheet" href="/css/mastodon-timeline.css" />
<script src="/js/mastodon-timeline.js"></script>
<!-- Internal templates -->
<link rel="stylesheet" href="{{ .Site.Params.staticPath }}/bootstrap-5/css/bootstrap.min.css" media="all">
<link rel="stylesheet" href="{{ .Site.Params.staticPath }}/css/header.css" media="all">
<link rel="stylesheet" href="{{ .Site.Params.staticPath }}/css/footer.css" media="all">
<!-- theme -->
<link rel="stylesheet" href="{{ .Site.Params.staticPath }}/css/theme.css" media="all">
<!-- Custom Styles -->
{{ if .Site.Params.customCSS }}
<link rel="stylesheet" href="{{ .Site.Params.staticPath }}/style.css">
{{ end }}
<style>
:root {
--text-color: {{ .Site.Params.color.textColor | default "#343a40" }};
--text-secondary-color: {{ .Site.Params.color.secondaryTextColor | default "#6c757d" }};
--background-color: {{ .Site.Params.color.backgroundColor | default "#eaedf0" }};
--secondary-background-color: {{ .Site.Params.color.secondaryBackgroundColor | default "#64ffda1a" }};
--primary-color: {{ .Site.Params.color.primaryColor | default "#007bff" }};
--secondary-color: {{ .Site.Params.color.secondaryColor | default "#f8f9fa" }};
/* dark mode colors */
--text-color-dark: {{ .Site.Params.color.darkmode.textColor | default "#e4e6eb" }};
--text-secondary-color-dark: {{ .Site.Params.color.darkmode.secondaryTextColor | default "#b0b3b8" }};
--background-color-dark: {{ .Site.Params.color.darkmode.backgroundColor | default "#18191a" }};
--secondary-background-color-dark: {{ .Site.Params.color.darkmode.secondaryBackgroundColor | default "#212529" }};
--primary-color-dark: {{ .Site.Params.color.darkmode.primaryColor | default "#ffffff" }};
--secondary-color-dark: {{ .Site.Params.color.darkmode.secondaryColor | default "#212529" }};
}
body {
font-size: {{ .Site.Params.font.fontSize | default "1rem" }};
font-weight: {{ .Site.Params.font.fontWeight | default "400" }};
line-height: {{ .Site.Params.font.lineHeight | default "1.5" }};
text-align: {{ .Site.Params.font.textAlign | default "left" }};
}
html {
background-color: var(--background-color) !important;
}
body::-webkit-scrollbar {
width: .5em;
height: .5em;
background-color: var(--background-color);
}
::-webkit-scrollbar-track {
box-shadow: inset 0 0 6px var(--background-color);
border-radius: 1rem;
}
::-webkit-scrollbar-thumb {
border-radius: 1rem;
background-color: var(--secondary-color);
outline: 1px solid var(--background-color);
}
#search-content::-webkit-scrollbar {
width: .5em;
height: .1em;
background-color: var(--background-color);
}
</style>

View File

@ -0,0 +1,9 @@
{{ if .Site.Params.about.enable | default false }}
<section id="about" class="py-0 py-sm-5">
<div class="mt-container">
<div id="mt-body" class="mt-body" role="feed">
<div class="loading-spinner"></div>
</div>
</div>
</section>
{{ end }}

View File

@ -0,0 +1,126 @@
{{ if .Site.Params.experience.enable | default false }}
<section id="experience" class="py-5">
<div class="container">
<h3 class="text-center">{{ .Site.Params.experience.title | default "Experience" }}</h3>
<div class="row justify-content-center">
<div class="col-sm-12 col-md-8 col-lg-8 py-5">
<div class="experience-container px-3 pt-2">
<ul class="nav nav-pills mb-3 bg-transparent primary-font" id="pills-tab" role="tablist">
{{ range $index, $element := .Site.Params.experience.items }}
{{ if (eq $index 0) }}
<li class="nav-item px-1 bg-transparent" role="presentation">
<div
class="nav-link active bg-transparent"
aria-selected="true"
role="tab"
data-bs-toggle="pill"
id='{{ replace .company " " "-" }}-{{ replace .date " " "-" }}-tab'
data-bs-target='#pills-{{ replace .company " " "-" }}-{{ replace .date " " "-" }}'
aria-controls='{{ replace .company " " "-" }}-{{ replace .date " " "-" }}'
>
{{ .company }}
</div>
</li>
{{ else }}
<li class="nav-item px-1 bg-transparent" role="presentation">
<div
class="nav-link bg-transparent"
aria-selected="true"
role="tab"
data-bs-toggle="pill"
id='{{ replace .company " " "-" }}-{{ replace .date " " "-" }}-tab'
data-bs-target='#pills-{{ replace .company " " "-" }}-{{ replace .date " " "-" }}'
aria-controls='{{ replace .company " " "-" }}-{{ replace .date " " "-" }}'
>
{{ .company }}
</div>
</li>
{{ end }}
{{ end }}
</ul>
<div class="tab-content pb-5 pt-2 bg-transparent primary-font" id="pills-tabContent">
{{ range $index, $element := .Site.Params.experience.items }}
{{ if (eq $index 0) }}
<div
class="tab-pane fade show active bg-transparent"
role="tabpanel"
id='pills-{{ replace .company " " "-" }}-{{ replace .date " " "-" }}'
aria-labelledby='pills-{{ replace .company " " "-" }}-{{ replace .date " " "-" }}-tab'
>
<div>
<span class="h4"><a href="{{ .companyUrl }}" target="_blank">{{ .job }}</a></span>
<div class="pb-1">
<small>{{ .date }}</small>
{{ if .info.enable | default true }}
<span class="p-2">
<span
style="cursor: pointer;"
data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-original-title={{ .info.content | default (print "Working as a " .job " at " .company ) }}
>
<i class="fas fa-info-circle fa-xs"></i>
</span>
</span>
{{ end }}
</div>
{{ if .featuredLink.enable | default false }}
<div class="py-2 featuredLink">
<a class="p-2 px-4 btn btn-outline-primary btn-sm" href={{ .featuredLink.url | default "#" }} target="_blank">
{{ .featuredLink.name | default "Featured Link" }}
</a>
</div>
{{ end }}
</div>
{{ .content | markdownify}}
</div>
{{ else }}
<div
class="tab-pane fade bg-transparent"
role="tabpanel"
id='pills-{{ replace .company " " "-" }}-{{ replace .date " " "-" }}'
aria-labelledby='pills-{{ replace .company " " "-" }}-{{ replace .date " " "-" }}-tab'
>
<div>
<span class="h4"><a href="{{ .companyUrl }}" target="_blank">{{ .job }}</a></span>
<div class="pb-1">
<small>{{ .date }}</small>
{{ if .info.enable | default true }}
<span class="p-2">
<span
style="cursor: pointer;"
data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-original-title={{ .info.content | default (print "Worked as a " .job " at " .company ) }}
>
<i class="fas fa-info-circle fa-xs"></i>
</span>
</span>
{{ end }}
</div>
{{ if .featuredLink.enable | default false }}
<div class="py-2 featuredLink">
<a class="p-2 px-4 btn btn-outline-primary btn-sm" href={{ .featuredLink.url | default "#" }} target="_blank">
{{ .featuredLink.name | default "Featured Link" }}
</a>
</div>
{{ end }}
</div>
<div class="pt-2">
{{ .content | markdownify}}
</div>
</div>
{{ end }}
{{ end }}
</div>
</div>
</div>
</div>
</div>
</section>
{{ end }}

View File

@ -0,0 +1,7 @@
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-md-4 text-center">
&copy; {{ now.Format "2006"}} {{ .Site.Params.copyright }} {{ .Site.Params.terms.copyright | default "All Rights Reserved" }}
</div>
</div>
</div>

View File

@ -0,0 +1,53 @@
{{ if .Site.Params.hero.enable | default false }}
<section id="hero" class="py-5 align-middle">
<div class="container px-3 px-sm-5 px-md-5 px-lg-5 pt-lg-3">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-8 content {{ if .Site.Params.animate }}animate{{ end }}" id="primary-font">
<span class="subtitle">
{{ .Site.Params.hero.intro }}
</span>
<h2>
{{ .Site.Params.hero.title }}
</h2>
<h3>
{{ .Site.Params.hero.subtitle }}
</h3>
<p class="hero-content">
{{ .Site.Params.hero.content | markdownify }}
</p>
<div class="row">
<div class="col-auto h-100">
{{ if .Site.Params.hero.button.enable }}
<a href="{{ .Site.Params.hero.button.url }}" class="btn" {{ cond .Site.Params.hero.button.download "download" "" }}
{{ if .Site.Params.hero.button.newPage | default true }}
target="_blank"
{{ end }}>
{{ .Site.Params.hero.button.name }}
</a>
{{ end }}
</div>
<div class="col-auto px-0 h-100">
{{- partial "sections/hero/social.html" . -}}
</div>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-4">
<div class="row justify-content-center">
<div class="col-sm-12 col-md-9 pt-5 image {{ if .Site.Params.animate }}animate{{ end }} px-5 px-md-5 px-lg-0 text-center">
<img src="{{ .Site.Params.hero.image }}"
class="img-thumbnail mx-auto{{ if .Site.Params.hero.roundImage }} rounded-circle{{ end }}"
alt=""
>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<div class="mt-container col-sm-12 col-md-12 col-lg-12">
<div id="mt-body" class="mt-body" role="feed">
<div class="loading-spinner"></div>
</div>
{{ end }}

View File

@ -0,0 +1,17 @@
<span>
{{ range .Site.Params.hero.socialLinks.fontAwesomeIcons }}
<span class="px-1">
<a href="{{ .url }}" target="_blank" class="btn social-icon">
<i class="{{ .icon }}"></i>
</a>
</span>
{{ end }}
{{ range .Site.Params.hero.socialLinks.customIcons }}
<span class="px-1">
<a href="{{ .url }}" target="_blank" class="btn social-icon">
<img src="{{ .icon }}">
</a>
</span>
{{ end }}
</span>