Merge pull request 'develop' (#5) from develop into main
Reviewed-on: #5
This commit is contained in:
commit
3b40368a24
156
docs/newsletter_integration_memo.md
Normal file
156
docs/newsletter_integration_memo.md
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
# 電子報介接備忘錄(租戶端 / Wagtail)
|
||||||
|
|
||||||
|
最後更新:2026-02-11
|
||||||
|
|
||||||
|
## 1. 目標與前提
|
||||||
|
|
||||||
|
本備忘錄用於整理租戶端(Wagtail)接下來的實作方向,重點是先完成可運行流程,再逐步補完細節。
|
||||||
|
|
||||||
|
## 2. 已確認決策(本次會議結論)
|
||||||
|
|
||||||
|
1. Member Center 雖為共用平台,但目前只有一個租戶。
|
||||||
|
2. 現階段「發確認信」與「訂閱/退訂頁面」先做在 Wagtail。
|
||||||
|
3. Wagtail 與 Member Center 的溝通以 API 為主。
|
||||||
|
4. SMTP relay 僅負責寄出一次,重送、token 安全、簽章等由其他系統或工具負責,不屬 Wagtail 責任。
|
||||||
|
5. 工作大項與順序固定為:
|
||||||
|
1. 準備工作(電子報系統設定)
|
||||||
|
2. 訂閱流程
|
||||||
|
3. 退訂流程
|
||||||
|
4. 電子報 app
|
||||||
|
5. 發信流程(排程)
|
||||||
|
|
||||||
|
## 3. 工作大項與執行順序(含範圍)
|
||||||
|
|
||||||
|
## 3.1 準備工作(電子報系統設定,第一優先)
|
||||||
|
|
||||||
|
範圍:
|
||||||
|
- 建立 Member Center 連線設定(base URL、tenant_id、list_id、API timeout 等)。
|
||||||
|
- 建立 Send Engine 連線設定(base URL、OAuth scope、API timeout 等)。
|
||||||
|
- 建立一般發信設定(SMTP relay、寄件者名稱/信箱、reply-to、預設編碼等)。
|
||||||
|
- 建立模板設定:
|
||||||
|
- 訂閱確認信 template
|
||||||
|
- 訂閱確認(成功)頁面 template
|
||||||
|
- 取消訂閱頁面 template
|
||||||
|
|
||||||
|
重點:
|
||||||
|
- 設定集中管理,避免散落在程式碼或多處後台欄位。
|
||||||
|
- 區分「系統連線設定」與「內容模板設定」,便於權限與維運。
|
||||||
|
|
||||||
|
完成標準(DoD):
|
||||||
|
- 可在單一設定入口完成上述設定並通過基本驗證。
|
||||||
|
- 模板可被後續訂閱/退訂流程直接引用。
|
||||||
|
|
||||||
|
## 3.2 訂閱流程(第二優先)
|
||||||
|
|
||||||
|
範圍:
|
||||||
|
- 在站台頁面區塊(暫定 Footer)提供 email 訂閱入口。
|
||||||
|
- 後端接收 email,呼叫 Member Center 訂閱 API。
|
||||||
|
- 由 Wagtail 送出訂閱確認信(透過 SMTP relay)。
|
||||||
|
- 使用者點信內連結回到 Wagtail 確認頁。
|
||||||
|
- 確認頁自動呼叫 Member Center 確認 API。
|
||||||
|
|
||||||
|
重點:
|
||||||
|
- 流程中 token 需一路帶入與驗證。
|
||||||
|
- 確認信內容採 HTML(可附純文字 fallback)。
|
||||||
|
|
||||||
|
完成標準(DoD):
|
||||||
|
- 可從站台成功發起訂閱。
|
||||||
|
- 可收到確認信並完成確認。
|
||||||
|
- 確認成功/失敗頁有明確訊息與追蹤記錄。
|
||||||
|
|
||||||
|
## 3.3 退訂流程(第三優先)
|
||||||
|
|
||||||
|
範圍:
|
||||||
|
- 電子報底部提供退訂連結,導向 Wagtail 退訂頁。
|
||||||
|
- 進入退訂頁時,先向 Member Center 申請退訂 token。
|
||||||
|
- 使用者按確認退訂後,呼叫 Member Center 退訂 API 完成流程。
|
||||||
|
|
||||||
|
重點:
|
||||||
|
- token 不做長期保存。
|
||||||
|
- 頁面需可處理 token 失效/不存在等錯誤狀態。
|
||||||
|
|
||||||
|
完成標準(DoD):
|
||||||
|
- 使用者能從信件連結完成退訂。
|
||||||
|
- 退訂結果頁可正確呈現成功/失敗狀態。
|
||||||
|
|
||||||
|
## 3.4 電子報 app(第四優先)
|
||||||
|
|
||||||
|
範圍:
|
||||||
|
- 在 Wagtail 後台提供電子報內容管理能力。
|
||||||
|
- 提供 HTML 編輯器建立/編修內容。
|
||||||
|
- 支援可替換參數(例如 token、email)。
|
||||||
|
|
||||||
|
重點:
|
||||||
|
- 編輯器中的連結參數以佔位符表示,發送前由 backend 替換。
|
||||||
|
- 若有 Member Center API 相關連結需在內容中可配置,也採相同參數機制。
|
||||||
|
|
||||||
|
完成標準(DoD):
|
||||||
|
- 編輯器可存草稿與更新內容。
|
||||||
|
- 內容中參數可在送出前正確替換。
|
||||||
|
|
||||||
|
## 3.5 發信流程(排程,第五優先)
|
||||||
|
|
||||||
|
範圍:
|
||||||
|
- 可建立發送任務與排程時間。
|
||||||
|
- 排程前可持續編輯內容與調整時間。
|
||||||
|
- 到時觸發時:先取 Member Center auth,再將內容包固定 header/footer,送到 Send Engine。
|
||||||
|
|
||||||
|
重點:
|
||||||
|
- 「內容定稿」與「實際送出」拆開。
|
||||||
|
- 發送前的模板組裝由 Wagtail backend 負責。
|
||||||
|
|
||||||
|
完成標準(DoD):
|
||||||
|
- 排程到點可成功建立 Send Engine send job。
|
||||||
|
- 失敗可記錄原因並可人工重試。
|
||||||
|
|
||||||
|
## 4. 參數替換規則(先行約定)
|
||||||
|
|
||||||
|
建議佔位符格式(待實作時可再定版):
|
||||||
|
- `{{token}}`
|
||||||
|
- `{{email}}`
|
||||||
|
- `{{list_id}}`
|
||||||
|
- `{{tenant_id}}`
|
||||||
|
- `{{confirm_url}}`
|
||||||
|
- `{{unsubscribe_url}}`
|
||||||
|
|
||||||
|
替換時機:
|
||||||
|
- 發信前 backend 最後一步統一替換。
|
||||||
|
|
||||||
|
注意事項:
|
||||||
|
- URL 參數需做 URL encode。
|
||||||
|
- 缺少必要參數時,中止發送並記錄可追蹤錯誤。
|
||||||
|
|
||||||
|
## 5. URL 與系統歸屬標註規範(文件撰寫規則)
|
||||||
|
|
||||||
|
後續文件凡提到網址,必須明確標註「是哪個系統」:
|
||||||
|
|
||||||
|
1. Wagtail(租戶站台)網址
|
||||||
|
- 例:`https://{tenant-site}/newsletter/confirm?token=...`
|
||||||
|
- 用途:使用者互動頁、站台前台/後台頁面。
|
||||||
|
|
||||||
|
2. Member Center API 網址
|
||||||
|
- 例:`https://{member-center}/newsletter/subscribe`
|
||||||
|
- 用途:訂閱、確認、退訂 token、退訂等 API。
|
||||||
|
|
||||||
|
3. Send Engine API 網址
|
||||||
|
- 例:`https://{send-engine}/api/send-jobs`
|
||||||
|
- 用途:建立/查詢/取消發信任務。
|
||||||
|
|
||||||
|
4. SMTP Relay 連線端點
|
||||||
|
- 用途:Wagtail 寄送確認信。
|
||||||
|
- 備註:僅作為寄送通道,不承擔重送與安全機制。
|
||||||
|
|
||||||
|
## 6. 非 Wagtail 責任邊界(本階段)
|
||||||
|
|
||||||
|
以下不納入目前 Wagtail 工作範圍:
|
||||||
|
- SMTP 後續重送策略。
|
||||||
|
- token 簽章與高階安全策略。
|
||||||
|
- 跨系統風控與防濫用機制。
|
||||||
|
|
||||||
|
## 7. 下一步(實作啟動清單)
|
||||||
|
|
||||||
|
1. 先完成電子報系統設定(Member Center / Send Engine / SMTP / 模板)。
|
||||||
|
2. 再完成訂閱流程 API 串接與確認頁。
|
||||||
|
3. 再完成退訂頁與退訂 API 串接。
|
||||||
|
4. 建立電子報 app 與 HTML 編輯器資料模型。
|
||||||
|
5. 最後接排程任務與 Send Engine 發信。
|
||||||
@ -23,6 +23,19 @@ BLOCK_SIZE = _get_env_int("HOMEPAGE_BLOCK_SIZE", 7) # Default to 7 articles in b
|
|||||||
HORIZON_SIZE = _get_env_int("HOMEPAGE_HORIZON_SIZE", 4) # Default to 4 articles in horizon layout
|
HORIZON_SIZE = _get_env_int("HOMEPAGE_HORIZON_SIZE", 4) # Default to 4 articles in horizon layout
|
||||||
PAGE_SIZE = _get_env_int("HOMEPAGE_PAGE_SIZE", 10) # Default to 10 articles per page for pagination
|
PAGE_SIZE = _get_env_int("HOMEPAGE_PAGE_SIZE", 10) # Default to 10 articles per page for pagination
|
||||||
CATEGORY_HOT_SIZE = _get_env_int("CATEGORY_HOT_SIZE", 6) # Default to 4 articles in category hot layout
|
CATEGORY_HOT_SIZE = _get_env_int("CATEGORY_HOT_SIZE", 6) # Default to 4 articles in category hot layout
|
||||||
|
RELATED_ARTICLES_SIZE = _get_env_int("RELATED_ARTICLES_SIZE", 4) # Default to 4 related articles
|
||||||
|
|
||||||
|
class BreadcrumbMixin:
|
||||||
|
def build_breadcrumbs(self):
|
||||||
|
site = self.get_site()
|
||||||
|
site_root = site.root_page if site else None
|
||||||
|
if site_root:
|
||||||
|
ancestors = self.get_ancestors().specific().filter(depth__gt=site_root.depth)
|
||||||
|
else:
|
||||||
|
ancestors = self.get_ancestors().specific()
|
||||||
|
breadcrumbs = list(ancestors) + [self]
|
||||||
|
return breadcrumbs, site_root
|
||||||
|
|
||||||
|
|
||||||
# Mixin for Category-related functionality
|
# Mixin for Category-related functionality
|
||||||
class CategoryMixin:
|
class CategoryMixin:
|
||||||
@ -70,17 +83,6 @@ class CategoryMixin:
|
|||||||
)
|
)
|
||||||
return blocks
|
return blocks
|
||||||
|
|
||||||
# Build breadcrumbs
|
|
||||||
def build_breadcrumbs(self):
|
|
||||||
site = self.get_site()
|
|
||||||
site_root = site.root_page if site else None
|
|
||||||
if site_root:
|
|
||||||
ancestors = self.get_ancestors().specific().filter(depth__gt=site_root.depth)
|
|
||||||
else:
|
|
||||||
ancestors = self.get_ancestors().specific()
|
|
||||||
breadcrumbs = list(ancestors) + [self]
|
|
||||||
return breadcrumbs, site_root
|
|
||||||
|
|
||||||
# Get latest articles
|
# Get latest articles
|
||||||
def get_latest_articles(self, request=None):
|
def get_latest_articles(self, request=None):
|
||||||
latest_page = LatestPage.objects.first()
|
latest_page = LatestPage.objects.first()
|
||||||
@ -196,7 +198,7 @@ class HomePage(Page, CategoryMixin):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class LatestPage(Page, CategoryMixin):
|
class LatestPage(Page, CategoryMixin, BreadcrumbMixin):
|
||||||
template = "home/category_page.html"
|
template = "home/category_page.html"
|
||||||
|
|
||||||
def get_context(self, request):
|
def get_context(self, request):
|
||||||
@ -208,7 +210,7 @@ class LatestPage(Page, CategoryMixin):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class TrendingPage(Page, CategoryMixin):
|
class TrendingPage(Page, CategoryMixin, BreadcrumbMixin):
|
||||||
template = "home/category_page.html"
|
template = "home/category_page.html"
|
||||||
|
|
||||||
def get_context(self, request):
|
def get_context(self, request):
|
||||||
@ -220,7 +222,7 @@ class TrendingPage(Page, CategoryMixin):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class CategoryPage(Page, CategoryMixin):
|
class CategoryPage(Page, CategoryMixin, BreadcrumbMixin):
|
||||||
@property
|
@property
|
||||||
def has_subcategories(self):
|
def has_subcategories(self):
|
||||||
return self.get_children().type(CategoryPage).live().exists()
|
return self.get_children().type(CategoryPage).live().exists()
|
||||||
@ -254,7 +256,7 @@ class ArticlePageTag(TaggedItemBase):
|
|||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
|
|
||||||
class ArticlePage(Page):
|
class ArticlePage(Page, BreadcrumbMixin):
|
||||||
cover_image = models.ForeignKey(
|
cover_image = models.ForeignKey(
|
||||||
"wagtailimages.Image",
|
"wagtailimages.Image",
|
||||||
null=True,
|
null=True,
|
||||||
@ -308,6 +310,16 @@ class ArticlePage(Page):
|
|||||||
def get_context(self, request):
|
def get_context(self, request):
|
||||||
context = super().get_context(request)
|
context = super().get_context(request)
|
||||||
|
|
||||||
|
breadcrumbs, site_root = self.build_breadcrumbs()
|
||||||
|
context["breadcrumbs"] = breadcrumbs
|
||||||
|
context["breadcrumb_root"] = site_root
|
||||||
|
category_crumbs = [
|
||||||
|
crumb
|
||||||
|
for crumb in breadcrumbs
|
||||||
|
if crumb.id != self.id and (not site_root or crumb.id != site_root.id)
|
||||||
|
]
|
||||||
|
context["article_category"] = category_crumbs[-1] if category_crumbs else None
|
||||||
|
|
||||||
tag_ids = list(self.tags.values_list("id", flat=True))
|
tag_ids = list(self.tags.values_list("id", flat=True))
|
||||||
if tag_ids:
|
if tag_ids:
|
||||||
related_articles = (
|
related_articles = (
|
||||||
@ -315,7 +327,7 @@ class ArticlePage(Page):
|
|||||||
.exclude(id=self.id)
|
.exclude(id=self.id)
|
||||||
.filter(tags__id__in=tag_ids)
|
.filter(tags__id__in=tag_ids)
|
||||||
.distinct()
|
.distinct()
|
||||||
.order_by("-date", "-id")[:4]
|
.order_by("-date", "-id")[:RELATED_ARTICLES_SIZE]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
related_articles = ArticlePage.objects.none()
|
related_articles = ArticlePage.objects.none()
|
||||||
|
|||||||
396
innovedus_cms/home/static/css/article_page.css
Normal file
396
innovedus_cms/home/static/css/article_page.css
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
.article-page {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 718px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-breadcrumbs {
|
||||||
|
margin: 12px 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-breadcrumbs ol {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-breadcrumbs li {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #0e1b4266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-breadcrumbs li + li::before {
|
||||||
|
content: "/";
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-breadcrumbs a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-title {
|
||||||
|
background-color: #0e1b42;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content img {
|
||||||
|
max-width: 718px;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content h1 {
|
||||||
|
font-size: 36px;
|
||||||
|
margin: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content h2 {
|
||||||
|
font-size: 32px;
|
||||||
|
margin: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content p {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content .intro {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content .body iframe {
|
||||||
|
display: block;
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content .body blockquote.instagram-media,
|
||||||
|
.article-content .body blockquote.twitter-tweet,
|
||||||
|
.article-content .body .fb-post,
|
||||||
|
.article-content .body .fb-video {
|
||||||
|
max-width: 100% !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
margin-left: auto !important;
|
||||||
|
margin-right: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content .date {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #0e1b4266;
|
||||||
|
margin: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content .date::after {
|
||||||
|
content: "";
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: #0e1b4266;
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-tags {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-tags .tag-chips {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chips li {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chips a {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 96px;
|
||||||
|
height: 25px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chips li:nth-child(3n + 1) a {
|
||||||
|
background: #00abf5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chips li:nth-child(3n + 2) a {
|
||||||
|
background: #f4a41c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chips li:nth-child(3n) a {
|
||||||
|
background: #ff461c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-share {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 64px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-share .icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
--fill-0: #0e1b4266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-share .icon .icon-cutout {
|
||||||
|
fill: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-articles {
|
||||||
|
margin-top: 32px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-articles h2 {
|
||||||
|
margin: 0 0 24px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0e1b4266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-articles-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-articles-list > hr {
|
||||||
|
width: 100%;
|
||||||
|
margin: 12px 0;
|
||||||
|
margin-inline: 0;
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid #0e1b4266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-article-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 194px 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-article-image {
|
||||||
|
display: block;
|
||||||
|
width: 194px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-article-image img {
|
||||||
|
width: 194px;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 133px;
|
||||||
|
display: block;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-article-date {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #0e1b4266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-article-title {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chips {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-article-tags {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-to-top {
|
||||||
|
position: fixed;
|
||||||
|
right: var(--back-to-top-right, 24px);
|
||||||
|
bottom: 24px;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #0e1b42;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translateY(8px);
|
||||||
|
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-to-top.is-visible {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-to-top__icon {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #d9deea;
|
||||||
|
box-shadow: 0 3px 8px rgba(14, 27, 66, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-to-top__icon::before,
|
||||||
|
.back-to-top__icon::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 24px;
|
||||||
|
width: 14px;
|
||||||
|
height: 2px;
|
||||||
|
background: #0e1b42;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-to-top__icon::before {
|
||||||
|
left: 12px;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-to-top__icon::after {
|
||||||
|
right: 12px;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-to-top__label {
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
|
.article-page {
|
||||||
|
max-width: 560px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content img {
|
||||||
|
max-width: 560px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 575px) and (max-width: 767px) {
|
||||||
|
.article-page {
|
||||||
|
max-width: 426px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content img {
|
||||||
|
max-width: 426px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-article-title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-to-top {
|
||||||
|
bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 574px) {
|
||||||
|
.article-page {
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content img {
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content .intro {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content .date {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-tags {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-article-item {
|
||||||
|
grid-template-columns: 139px 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-article-image {
|
||||||
|
width: 139px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-article-image img {
|
||||||
|
width: 139px;
|
||||||
|
height: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-article-date {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-article-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chips a {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-article-tags {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-to-top {
|
||||||
|
bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-to-top__label {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.article-breadcrumbs {
|
||||||
|
margin: 8px 0 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +1,3 @@
|
|||||||
.block-title {
|
|
||||||
display: inline-block;
|
|
||||||
width: 197px;
|
|
||||||
height: 87px;
|
|
||||||
vertical-align: middle;
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-title span {
|
|
||||||
padding-left: 21px;
|
|
||||||
line-height: 87px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-title {
|
.category-title {
|
||||||
background-color: #00abf5;
|
background-color: #00abf5;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
@ -50,17 +36,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 574px) {
|
@media (max-width: 574px) {
|
||||||
.block-title {
|
|
||||||
width: 139px;
|
|
||||||
height: 55px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-title span {
|
|
||||||
padding-left: 14px;
|
|
||||||
line-height: 55px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subcategory-title span {
|
.subcategory-title span {
|
||||||
width: 139px;
|
width: 139px;
|
||||||
height: 55px;
|
height: 55px;
|
||||||
|
|||||||
@ -13,20 +13,6 @@
|
|||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-title {
|
|
||||||
display: inline-block;
|
|
||||||
width: 197px;
|
|
||||||
height: 87px;
|
|
||||||
vertical-align: middle;
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-title span {
|
|
||||||
padding-left: 21px;
|
|
||||||
line-height: 87px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-title-divider {
|
.block-title-divider {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
@ -78,17 +64,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 574px) {
|
@media (max-width: 574px) {
|
||||||
.block-title {
|
|
||||||
width: 139px;
|
|
||||||
height: 55px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-title span {
|
|
||||||
padding-left: 14px;
|
|
||||||
line-height: 55px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-title-divider {
|
.block-title-divider {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
|
|||||||
67
innovedus_cms/home/static/js/article_page.js
Normal file
67
innovedus_cms/home/static/js/article_page.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
(function () {
|
||||||
|
const button = document.querySelector(".back-to-top");
|
||||||
|
const articlePage = document.querySelector(".article-page");
|
||||||
|
const articleBody = document.querySelector(".article-content .body");
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
|
const toggleVisibility = () => {
|
||||||
|
button.classList.toggle("is-visible", window.scrollY > 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncHorizontalPosition = () => {
|
||||||
|
if (!articlePage) return;
|
||||||
|
const rightGap = Math.max(0, window.innerWidth - articlePage.getBoundingClientRect().right);
|
||||||
|
button.style.setProperty("--back-to-top-right", `${rightGap}px`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeEmbedsResponsive = () => {
|
||||||
|
if (!articleBody) return;
|
||||||
|
|
||||||
|
articleBody.querySelectorAll("iframe").forEach((iframe) => {
|
||||||
|
const src = (iframe.getAttribute("src") || "").toLowerCase();
|
||||||
|
const isFixedRatioPlayer =
|
||||||
|
src.includes("youtube.com") ||
|
||||||
|
src.includes("youtube-nocookie.com") ||
|
||||||
|
src.includes("m.youtube.com") ||
|
||||||
|
src.includes("youtu.be") ||
|
||||||
|
src.includes("vimeo.com") ||
|
||||||
|
src.includes("player.vimeo.com") ||
|
||||||
|
src.includes("dailymotion.com");
|
||||||
|
const width = Number(iframe.getAttribute("width"));
|
||||||
|
const height = Number(iframe.getAttribute("height"));
|
||||||
|
|
||||||
|
iframe.style.width = "100%";
|
||||||
|
iframe.style.maxWidth = "100%";
|
||||||
|
|
||||||
|
if (isFixedRatioPlayer) {
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
iframe.style.aspectRatio = `${width} / ${height}`;
|
||||||
|
} else {
|
||||||
|
iframe.style.aspectRatio = "16 / 9";
|
||||||
|
}
|
||||||
|
iframe.style.height = "auto";
|
||||||
|
} else {
|
||||||
|
iframe.style.removeProperty("aspect-ratio");
|
||||||
|
iframe.style.removeProperty("height");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("scroll", toggleVisibility, { passive: true });
|
||||||
|
window.addEventListener("resize", syncHorizontalPosition);
|
||||||
|
|
||||||
|
toggleVisibility();
|
||||||
|
syncHorizontalPosition();
|
||||||
|
makeEmbedsResponsive();
|
||||||
|
|
||||||
|
if (articleBody && "MutationObserver" in window) {
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
makeEmbedsResponsive();
|
||||||
|
});
|
||||||
|
observer.observe(articleBody, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
button.addEventListener("click", function () {
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
});
|
||||||
|
})();
|
||||||
@ -1,35 +1,125 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load wagtailcore_tags wagtailimages_tags %}
|
{% load wagtailcore_tags wagtailimages_tags static %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static 'css/article_page.css' %}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<article>
|
<article class="article-page">
|
||||||
<h1>{{ page.title }}</h1>
|
{% if breadcrumbs %}
|
||||||
{% if page.banner_image %}
|
<nav class="article-breadcrumbs" aria-label="breadcrumb">
|
||||||
{% image page.banner_image original as banner %}
|
<ol>
|
||||||
<img src="{{ banner.url }}" alt="{{ page.title }}">
|
<li>
|
||||||
|
{% if breadcrumb_root %}
|
||||||
|
<a href="{{ breadcrumb_root.url }}">首頁</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="/">首頁</a>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% for crumb in breadcrumbs %}
|
||||||
|
{% if not breadcrumb_root or crumb.id != breadcrumb_root.id %}
|
||||||
|
{% if crumb.id != page.id %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p class="date">{{ page.date }}</p>
|
<div class="article-category">
|
||||||
<div class="intro">{{ page.intro }}</div>
|
<div class="block-title category-title"><span>{% if article_category %}{{ article_category.title }}{% endif %}</span></div>
|
||||||
<div class="body">
|
|
||||||
{{ page.body }}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="article-content">
|
||||||
|
{% if page.cover_image %}
|
||||||
|
{% image page.cover_image original as cover %}
|
||||||
|
<img src="{{ cover.url }}" alt="{{ page.title }}">
|
||||||
|
{% endif %}
|
||||||
|
<h1>{{ page.title }}</h1>
|
||||||
|
<p class="date">{{ page.date|date:"Y/m/d" }}</p>
|
||||||
|
<div class="intro">{{ page.intro }}</div>
|
||||||
|
<div class="body">
|
||||||
|
{{ page.body }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% with share_url=request.build_absolute_uri|urlencode share_text=page.title|urlencode %}
|
||||||
|
<div class="article-share" aria-label="分享文章">
|
||||||
|
<a href="https://www.facebook.com/sharer/sharer.php?u={{ share_url }}" target="_blank" rel="noopener noreferrer" aria-label="分享到 Facebook">
|
||||||
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 33.18 33.18" role="img">
|
||||||
|
<path d="M16.59 0C7.43 0 0 7.43 0 16.59C0 25.75 7.43 33.18 16.59 33.18C25.75 33.18 33.18 25.75 33.18 16.59C33.18 7.43 25.75 0 16.59 0ZM23.12 8.37H21.18C19.27 8.37 18.67 9.56 18.67 10.78V13.67H22.94L22.26 18.12H18.67V28.89H13.85V18.12H9.94V13.67H13.85V10.28C13.85 6.42 16.15 4.29 19.67 4.29C21.36 4.29 23.12 4.59 23.12 4.59V8.38V8.37Z" fill="var(--fill-0, #0E1B42)"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a href="https://x.com/intent/tweet?url={{ share_url }}&text={{ share_text }}" target="_blank" rel="noopener noreferrer" aria-label="分享到 X">
|
||||||
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 33.18 33.18" role="img">
|
||||||
|
<path d="M16.59 0C7.43 0 0 7.43 0 16.59C0 25.75 7.43 33.18 16.59 33.18C25.75 33.18 33.18 25.75 33.18 16.59C33.18 7.43 25.75 0 16.59 0Z" fill="var(--fill-0, #0E1B42)"/>
|
||||||
|
<path class="icon-cutout" d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865z" transform="translate(7.59 7.59) scale(1.125)"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.threads.net/intent/post?text={{ share_text }}%20{{ share_url }}" target="_blank" rel="noopener noreferrer" aria-label="分享到 Threads">
|
||||||
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 33.18 33.18" role="img">
|
||||||
|
<path d="M16.59 0C7.43 0 0 7.43 0 16.59C0 25.75 7.43 33.18 16.59 33.18C25.75 33.18 33.18 25.75 33.18 16.59C33.18 7.43 25.75 0 16.59 0Z" fill="var(--fill-0, #0E1B42)"/>
|
||||||
|
<path class="icon-cutout" d="M6.321 6.016c-.27-.18-1.166-.802-1.166-.802.756-1.081 1.753-1.502 3.132-1.502.975 0 1.803.327 2.394.948s.928 1.509 1.005 2.644q.492.207.905.484c1.109.745 1.719 1.86 1.719 3.137 0 2.716-2.226 5.075-6.256 5.075C4.594 16 1 13.987 1 7.994 1 2.034 4.482 0 8.044 0 9.69 0 13.55.243 15 5.036l-1.36.353C12.516 1.974 10.163 1.43 8.006 1.43c-3.565 0-5.582 2.171-5.582 6.79 0 4.143 2.254 6.343 5.63 6.343 2.777 0 4.847-1.443 4.847-3.556 0-1.438-1.208-2.127-1.27-2.127-.236 1.234-.868 3.31-3.644 3.31-1.618 0-3.013-1.118-3.013-2.582 0-2.09 1.984-2.847 3.55-2.847.586 0 1.294.04 1.663.114 0-.637-.54-1.728-1.9-1.728-1.25 0-1.566.405-1.967.868ZM8.716 8.19c-2.04 0-2.304.87-2.304 1.416 0 .878 1.043 1.168 1.6 1.168 1.02 0 2.067-.282 2.232-2.423a6.2 6.2 0 0 0-1.528-.161" transform="translate(7.59 7.59) scale(1.125)"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
{% with tags=page.tags.all %}
|
{% with tags=page.tags.all %}
|
||||||
{% if tags %}
|
{% if tags %}
|
||||||
<div class="tags">
|
<div class="article-tags">
|
||||||
<span>Hashtags:</span>
|
<ul class="tag-chips">
|
||||||
<ul>
|
|
||||||
{% for tag in tags %}
|
{% for tag in tags %}
|
||||||
<li><a href="{% url 'hashtag_search' tag.slug %}">#{{ tag }}</a></li>
|
<li><a href="{% url 'hashtag_search' tag.slug %}">{{ tag }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% if related_articles %}
|
{% if related_articles %}
|
||||||
|
<hr/>
|
||||||
<section class="related-articles">
|
<section class="related-articles">
|
||||||
<h2>相關文章</h2>
|
<h2>相關文章</h2>
|
||||||
{% include "home/includes/article_list.html" with items=related_articles %}
|
<div class="related-articles-list">
|
||||||
|
{% for related in related_articles %}
|
||||||
|
<article class="related-article-item">
|
||||||
|
<div class="related-article-left">
|
||||||
|
<a class="related-article-image" href="{{ related.url }}">
|
||||||
|
{% if related.cover_image %}
|
||||||
|
{% image related.cover_image max-194x133 as related_cover %}
|
||||||
|
<img src="{{ related_cover.url }}" alt="{{ related.title }}">
|
||||||
|
{% else %}
|
||||||
|
<img src="{% static 'img/default_cover.jpg' %}" alt="{{ related.title }}">
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
<p class="related-article-date">{{ related.date|date:"Y/m/d" }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="related-article-right">
|
||||||
|
<h3 class="related-article-title"><a href="{{ related.url }}">{{ related.title }}</a></h3>
|
||||||
|
{% with related_tags=related.tags.all %}
|
||||||
|
{% if related_tags %}
|
||||||
|
<ul class="related-article-tags tag-chips">
|
||||||
|
{% for tag in related_tags %}
|
||||||
|
<li><a href="{% url 'hashtag_search' tag.slug %}">{{ tag }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<hr/>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</article>
|
</article>
|
||||||
|
<button class="back-to-top" type="button" aria-label="回到頁首">
|
||||||
|
<span class="back-to-top__icon" aria-hidden="true"></span>
|
||||||
|
<span class="back-to-top__label">TOP</span>
|
||||||
|
</button>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script type="text/javascript" src="{% static 'js/article_page.js' %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
7
innovedus_cms/mysite/context_processors.py
Normal file
7
innovedus_cms/mysite/context_processors.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
def ga4(request):
|
||||||
|
return {
|
||||||
|
"ga4_measurement_id": getattr(settings, "GA4_MEASUREMENT_ID", ""),
|
||||||
|
}
|
||||||
@ -26,6 +26,9 @@ def env_list(name, default):
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
GA4_MEASUREMENT_ID = os.environ.get("GA4_MEASUREMENT_ID", "").strip()
|
||||||
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||||
|
|
||||||
@ -87,6 +90,7 @@ TEMPLATES = [
|
|||||||
"django.contrib.messages.context_processors.messages",
|
"django.contrib.messages.context_processors.messages",
|
||||||
"wagtail.contrib.settings.context_processors.settings",
|
"wagtail.contrib.settings.context_processors.settings",
|
||||||
"home.context_processors.navigation_pages",
|
"home.context_processors.navigation_pages",
|
||||||
|
"mysite.context_processors.ga4",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -369,6 +369,20 @@ footer .footer-sections {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.block-title {
|
||||||
|
display: inline-block;
|
||||||
|
width: 197px;
|
||||||
|
height: 87px;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-title span {
|
||||||
|
padding-left: 21px;
|
||||||
|
line-height: 87px;
|
||||||
|
}
|
||||||
|
|
||||||
.template-darkbackground .site-hero-band {
|
.template-darkbackground .site-hero-band {
|
||||||
background-color: #0e1b42;
|
background-color: #0e1b42;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
@ -578,6 +592,17 @@ footer .footer-sections {
|
|||||||
.main-nav {
|
.main-nav {
|
||||||
right: 16px;
|
right: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.block-title {
|
||||||
|
width: 139px;
|
||||||
|
height: 55px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-title span {
|
||||||
|
padding-left: 14px;
|
||||||
|
line-height: 55px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer figreset {
|
@layer figreset {
|
||||||
|
|||||||
@ -17,6 +17,15 @@
|
|||||||
<meta name="description" content="{{ page.search_description }}" />
|
<meta name="description" content="{{ page.search_description }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
{% if ga4_measurement_id %}
|
||||||
|
<script async src="https://www.googletagmanager.com/gtag/js?id={{ ga4_measurement_id }}"></script>
|
||||||
|
<script>
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
gtag('config', '{{ ga4_measurement_id }}');
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{# Force all links in the live preview panel to be opened in a new tab #}
|
{# Force all links in the live preview panel to be opened in a new tab #}
|
||||||
{% if request.in_preview_panel %}
|
{% if request.in_preview_panel %}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user