Compare commits

..

No commits in common. "be93761b487e035a83f4acae133ae760872a8fc0" and "78f29a14cdc0d05abf005e057eb2195d8d6dbbb2" have entirely different histories.

73 changed files with 325 additions and 3634 deletions

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
.env .env
__pycache__ __pycache__
*.pyc *.pyc
media/

View File

@ -1,8 +1,3 @@
fly.toml fly.toml
.git/ .git/
.venv
__pycache__/
*.pyc
*.sqlite3 *.sqlite3
media/
*.log

View File

@ -2,29 +2,20 @@ ARG PYTHON_VERSION=3.13-slim
FROM python:${PYTHON_VERSION} FROM python:${PYTHON_VERSION}
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE 1
PYTHONUNBUFFERED=1 \ ENV PYTHONUNBUFFERED 1
PIP_NO_CACHE_DIR=on \
DJANGO_SETTINGS_MODULE=mysite.settings.production RUN mkdir -p /code
WORKDIR /code WORKDIR /code
# Create an unprivileged user to run the app
RUN adduser --disabled-password --gecos '' app
COPY requirements.txt /tmp/requirements.txt COPY requirements.txt /tmp/requirements.txt
RUN set -ex && \ RUN set -ex && \
pip install --upgrade pip && \ pip install --upgrade pip && \
pip install -r /tmp/requirements.txt && \ pip install -r /tmp/requirements.txt && \
rm -rf /root/.cache/ rm -rf /root/.cache/
COPY . /code COPY . /code
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh && chown -R app:app /code
USER app
EXPOSE 8000 EXPOSE 8000
ENTRYPOINT ["/entrypoint.sh"]
CMD ["gunicorn","--bind",":8000","--workers","2","mysite.wsgi"] CMD ["gunicorn","--bind",":8000","--workers","2","mysite.wsgi"]

View File

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class BaseConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'base'

View File

@ -1,55 +0,0 @@
# Generated by Django 5.2.7 on 2025-10-15 03:30
import django.db.models.deletion
import uuid
import wagtail.fields
import wagtail.models.preview
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('wagtailcore', '0095_groupsitepermission'),
]
operations = [
migrations.CreateModel(
name='NavigationSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('linkedin_url', models.URLField(blank=True, verbose_name='LinkedIn URL')),
('github_url', models.URLField(blank=True, verbose_name='GitHub URL')),
('mastodon_url', models.URLField(blank=True, verbose_name='Mastodon URL')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='FooterText',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('translation_key', models.UUIDField(default=uuid.uuid4, editable=False)),
('live', models.BooleanField(default=True, editable=False, verbose_name='live')),
('has_unpublished_changes', models.BooleanField(default=False, editable=False, verbose_name='has unpublished changes')),
('first_published_at', models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='first published at')),
('last_published_at', models.DateTimeField(editable=False, null=True, verbose_name='last published at')),
('go_live_at', models.DateTimeField(blank=True, null=True, verbose_name='go live date/time')),
('expire_at', models.DateTimeField(blank=True, null=True, verbose_name='expiry date/time')),
('expired', models.BooleanField(default=False, editable=False, verbose_name='expired')),
('body', wagtail.fields.RichTextField()),
('latest_revision', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailcore.revision', verbose_name='latest revision')),
('live_revision', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailcore.revision', verbose_name='live revision')),
('locale', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='wagtailcore.locale', verbose_name='locale')),
],
options={
'verbose_name_plural': 'Footer Text',
'abstract': False,
'unique_together': {('translation_key', 'locale')},
},
bases=(wagtail.models.preview.PreviewableMixin, models.Model),
),
]

View File

@ -1,59 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-26 08:11
import django.db.models.deletion
import wagtail.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0001_initial'),
('wagtailimages', '0027_image_description'),
]
operations = [
migrations.CreateModel(
name='SocialMediaSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('links', wagtail.fields.StreamField([('link', 2)], block_lookup={0: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('facebook', 'Facebook'), ('twitter', 'Twitter'), ('instagram', 'Instagram'), ('thread', 'Thread'), ('linkedin', 'LinkedIn'), ('youtube', 'YouTube')]}), 1: ('wagtail.blocks.URLBlock', (), {}), 2: ('wagtail.blocks.StructBlock', [[('platform', 0), ('url', 1)]], {})})),
],
options={
'abstract': False,
},
),
migrations.AlterModelOptions(
name='navigationsettings',
options={'verbose_name': 'Footer Navigation'},
),
migrations.RemoveField(
model_name='navigationsettings',
name='github_url',
),
migrations.RemoveField(
model_name='navigationsettings',
name='linkedin_url',
),
migrations.RemoveField(
model_name='navigationsettings',
name='mastodon_url',
),
migrations.AddField(
model_name='navigationsettings',
name='footer_links',
field=wagtail.fields.StreamField([('section', 5)], blank=True, block_lookup={0: ('wagtail.blocks.CharBlock', (), {'required': False}), 1: ('wagtail.blocks.CharBlock', (), {}), 2: ('wagtail.blocks.URLBlock', (), {}), 3: ('wagtail.blocks.StructBlock', [[('label', 1), ('url', 2)]], {}), 4: ('wagtail.blocks.ListBlock', (3,), {}), 5: ('wagtail.blocks.StructBlock', [[('title', 0), ('links', 4)]], {})}, null=True),
),
migrations.CreateModel(
name='HeaderSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('site_name', models.CharField(blank=True, max_length=255)),
('extra_links', wagtail.fields.StreamField([('link', 2)], blank=True, block_lookup={0: ('wagtail.blocks.CharBlock', (), {}), 1: ('wagtail.blocks.URLBlock', (), {}), 2: ('wagtail.blocks.StructBlock', [[('label', 0), ('url', 1)]], {})}, null=True)),
('logo', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image')),
],
options={
'verbose_name': 'Header Settings',
},
),
]

View File

@ -1,39 +0,0 @@
# Generated by Django 5.2.7 on 2026-01-09 05:52
import django.db.models.deletion
import wagtail.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0002_socialmediasettings_alter_navigationsettings_options_and_more'),
('wagtailimages', '0027_image_description'),
]
operations = [
migrations.AlterField(
model_name='socialmediasettings',
name='links',
field=wagtail.fields.StreamField([('link', 2)], block_lookup={0: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('facebook', 'Facebook'), ('twitter', 'Twitter'), ('instagram', 'Instagram'), ('threads', 'Threads'), ('linkedin', 'LinkedIn'), ('youtube', 'YouTube')]}), 1: ('wagtail.blocks.URLBlock', (), {}), 2: ('wagtail.blocks.StructBlock', [[('platform', 0), ('url', 1)]], {})}),
),
migrations.CreateModel(
name='BannerSnippet',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(blank=True, help_text='識別用 key例如 home / category', max_length=50)),
('title', models.CharField(blank=True, max_length=255)),
('link_url', models.URLField(blank=True)),
('link_text', models.CharField(blank=True, max_length=100)),
('is_active', models.BooleanField(default=True)),
('sort_order', models.PositiveIntegerField(default=0)),
('image', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image')),
],
options={
'verbose_name': 'Banner',
'verbose_name_plural': 'Banners',
'ordering': ['sort_order', 'id'],
},
),
]

View File

@ -1,29 +0,0 @@
# Generated by Django 5.2.7 on 2026-01-09 09:01
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0003_alter_socialmediasettings_links_bannersnippet'),
('wagtailimages', '0027_image_description'),
]
operations = [
migrations.RemoveField(
model_name='headersettings',
name='logo',
),
migrations.AddField(
model_name='headersettings',
name='logo_dark',
field=models.ForeignKey(blank=True, help_text='淺色底用(深色 logo', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image'),
),
migrations.AddField(
model_name='headersettings',
name='logo_light',
field=models.ForeignKey(blank=True, help_text='深色底用(亮色 logo', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image'),
),
]

View File

@ -1,175 +0,0 @@
from django.db import models
from wagtail.admin.panels import (
FieldPanel,
MultiFieldPanel,
# import PublishingPanel:
PublishingPanel,
)
# import RichTextField:
from wagtail.fields import RichTextField
# import DraftStateMixin, PreviewableMixin, RevisionMixin, TranslatableMixin:
from wagtail.models import (
DraftStateMixin,
PreviewableMixin,
RevisionMixin,
TranslatableMixin,
)
from wagtail.contrib.settings.models import (
BaseGenericSetting,
register_setting,
)
from wagtail.snippets.models import register_snippet
from wagtail.fields import StreamField
from wagtail import blocks
@register_setting
class HeaderSettings(BaseGenericSetting):
logo_light = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
help_text="深色底用(亮色 logo",
)
logo_dark = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
help_text="淺色底用(深色 logo",
)
site_name = models.CharField(max_length=255, blank=True)
extra_links = StreamField([
("link", blocks.StructBlock([
("label", blocks.CharBlock()),
("url", blocks.URLBlock())
]))
], use_json_field=True, blank=True, null=True)
panels = [
MultiFieldPanel(
[
FieldPanel("logo_light"),
FieldPanel("logo_dark"),
FieldPanel("site_name"),
FieldPanel("extra_links"),
],
heading="Header Settings",
),
]
class Meta:
verbose_name = "Header Settings"
@register_setting
class NavigationSettings(BaseGenericSetting):
footer_links = StreamField([
("section", blocks.StructBlock([
("title", blocks.CharBlock(required=False)),
("links", blocks.ListBlock(blocks.StructBlock([
("label", blocks.CharBlock()),
("url", blocks.URLBlock())
]))),
]))
], use_json_field=True, blank=True, null=True)
panels = [
FieldPanel("footer_links"),
]
class Meta:
verbose_name = "Footer Navigation"
class SocialLinkBlock(blocks.StructBlock):
SOCIAL_MEDIA_CHOICES = [
("facebook", "Facebook"),
("twitter", "Twitter"),
("instagram", "Instagram"),
("threads", "Threads"),
("linkedin", "LinkedIn"),
("youtube", "YouTube"),
]
platform = blocks.ChoiceBlock(choices=SOCIAL_MEDIA_CHOICES)
url = blocks.URLBlock()
class Meta:
icon = "link"
label = "Social Link"
@register_setting
class SocialMediaSettings(BaseGenericSetting):
links = StreamField([
("link", SocialLinkBlock()),
], use_json_field=True)
panels = [FieldPanel("links")]
@register_snippet
class BannerSnippet(models.Model):
key = models.CharField(max_length=50, blank=True, help_text="識別用 key例如 home / category")
title = models.CharField(max_length=255, blank=True)
image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=False,
on_delete=models.SET_NULL,
related_name="+",
)
link_url = models.URLField(blank=True)
link_text = models.CharField(max_length=100, blank=True)
is_active = models.BooleanField(default=True)
sort_order = models.PositiveIntegerField(default=0)
panels = [
FieldPanel("key"),
FieldPanel("title"),
FieldPanel("image"),
FieldPanel("link_url"),
FieldPanel("link_text"),
FieldPanel("is_active"),
FieldPanel("sort_order"),
]
class Meta:
ordering = ["sort_order", "id"]
verbose_name = "Banner"
verbose_name_plural = "Banners"
def __str__(self):
return self.title or f"Banner {self.pk}"
@register_snippet
class FooterText(
DraftStateMixin,
RevisionMixin,
PreviewableMixin,
TranslatableMixin,
models.Model,
):
body = RichTextField()
panels = [
FieldPanel("body"),
PublishingPanel(),
]
def __str__(self):
return "Footer text"
def get_preview_template(self, request, mode_name):
return "base.html"
def get_preview_context(self, request, mode_name):
return {"footer_text": self.body}
class Meta(TranslatableMixin.Meta):
verbose_name_plural = "Footer Text"

View File

@ -1,27 +0,0 @@
{% load wagtailimages_tags banner_tags %}
{% get_banners banner_key as banners %}
{% if banners %}
<div class="banner-snippets">
{% for banner in banners %}
<div class="banner-snippets__item">
{% if banner.link_url %}
<a class="banner-snippets__link" href="{{ banner.link_url }}">
{% image banner.image fill-1200x300 alt=banner.title %}
{% if banner.title %}
<span class="banner-snippets__title">{{ banner.title }}</span>
{% endif %}
{% if banner.link_text %}
<span class="banner-snippets__cta">{{ banner.link_text }}</span>
{% endif %}
</a>
{% else %}
{% image banner.image fill-1200x300 alt=banner.title %}
{% if banner.title %}
<span class="banner-snippets__title">{{ banner.title }}</span>
{% endif %}
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}

View File

@ -1,5 +0,0 @@
{% load wagtailcore_tags %}
<div>
{{ footer_text|richtext }}
</div>

View File

@ -1,20 +0,0 @@
{% load wagtailimages_tags banner_tags %}
{% get_banners "home" first=True as banner %}
{% if banner %}
<div class="home-banner">
{% if banner.link_url %}
<a class="home-banner__link" href="{{ banner.link_url }}">
{% image banner.image width-1280 alt=banner.title %}
{% if banner.title %}
<span class="home-banner__title">{{ banner.title }}</span>
{% endif %}
{% if banner.link_text %}
<span class="home-banner__cta">{{ banner.link_text }}</span>
{% endif %}
</a>
{% else %}
{% image banner.image width-1280 alt=banner.title %}
{% endif %}
</div>
{% endif %}

View File

@ -1,15 +0,0 @@
from django import template
from base.models import BannerSnippet
register = template.Library()
@register.simple_tag
def get_banners(key=None, first=False):
banners = BannerSnippet.objects.filter(is_active=True, image__isnull=False)
if key:
banners = banners.filter(key=key)
if first:
return banners.first()
return banners

View File

@ -1,18 +0,0 @@
from django import template
from base.models import FooterText
register = template.Library()
@register.inclusion_tag("base/includes/footer_text.html", takes_context=True)
def get_footer_text(context):
footer_text = context.get("footer_text", "")
if not footer_text:
instance = FooterText.objects.filter(live=True).first()
footer_text = instance.body if instance else ""
return {
"footer_text": footer_text,
}

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

View File

@ -1,8 +0,0 @@
#!/usr/bin/env bash
set -e
# Run pending migrations and collect static assets before starting the app
python manage.py migrate --noinput
python manage.py collectstatic --noinput
exec "$@"

View File

@ -1,58 +0,0 @@
from django.core.exceptions import ValidationError
from wagtail.embeds.blocks import EmbedBlock
from wagtail.embeds import embeds as wagtail_embeds
from wagtail import blocks
from urllib.parse import urlparse
from django.conf import settings
class ValidatingEmbedBlock(EmbedBlock):
"""
Embed block that validates the URL at clean-time by resolving
via Wagtail's embed system. Raises ValidationError if not embeddable.
"""
def clean(self, value):
value = super().clean(value)
if value:
# Inherit base validation (already run via super().clean), and add
# optional network validation for selected providers.
url_str = value if isinstance(value, str) else getattr(value, "url", None)
if not url_str:
return value
host = (urlparse(url_str).hostname or "").lower()
strict_hosts = getattr(
settings,
"EMBED_STRICT_HOSTS",
("instagram.com", "facebook.com"),
)
validate_all = getattr(settings, "EMBED_STRICT_VALIDATE_ALL", False)
def _matches(h: str) -> bool:
return host == h or host.endswith("." + h)
must_validate = bool(validate_all or any(_matches(h) for h in strict_hosts))
if must_validate:
try:
# Attempt to resolve and cache embed; will raise on failure
wagtail_embeds.get_embed(url_str)
except Exception:
raise ValidationError(
"嵌入連結無法驗證(需公開且有權杖)。請確認 URL 與設定。"
)
return value
class H2HeadingBlock(blocks.CharBlock):
class Meta:
template = "home/blocks/h2_heading.html"
icon = "title"
label = "Heading"
class HorizontalRuleBlock(blocks.StaticBlock):
class Meta:
template = "home/blocks/horizontal_rule.html"
icon = "horizontalrule"
label = "Separator"

View File

@ -1,11 +0,0 @@
from home.models import LatestPage, TrendingPage
def navigation_pages(request):
"""
Provide Latest/Trending page references for site-wide navigation.
"""
return {
"nav_latest_page": LatestPage.objects.live().first(),
"nav_trending_page": TrendingPage.objects.live().first(),
}

View File

@ -1,83 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-26 08:11
import django.db.models.deletion
import home.models
import modelcluster.contrib.taggit
import modelcluster.fields
import wagtail.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0002_create_homepage'),
('taggit', '0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx'),
('wagtailcore', '0095_groupsitepermission'),
('wagtailimages', '0027_image_description'),
]
operations = [
migrations.CreateModel(
name='CategoryPage',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
],
options={
'abstract': False,
},
bases=('wagtailcore.page', home.models.CategoryMixin),
),
migrations.CreateModel(
name='LatestPage',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
],
options={
'abstract': False,
},
bases=('wagtailcore.page', home.models.CategoryMixin),
),
migrations.CreateModel(
name='TrendingPage',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
],
options={
'abstract': False,
},
bases=('wagtailcore.page', home.models.CategoryMixin),
),
migrations.CreateModel(
name='ArticlePage',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
('date', models.DateTimeField(verbose_name='Published date')),
('intro', models.CharField(blank=True, max_length=250)),
('body', wagtail.fields.StreamField([('heading', 0), ('paragraph', 1), ('image', 2), ('embed', 3), ('hr', 4), ('html', 5)], block_lookup={0: ('home.blocks.H2HeadingBlock', (), {'form_classname': 'full title'}), 1: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link']}), 2: ('wagtail.images.blocks.ImageChooserBlock', (), {}), 3: ('home.blocks.ValidatingEmbedBlock', (), {}), 4: ('home.blocks.HorizontalRuleBlock', (), {}), 5: ('wagtail.blocks.RawHTMLBlock', (), {'help_text': '僅限信任來源的 blockquote/iframe 原始碼'})})),
('trending', models.BooleanField(default=False, help_text='在熱門區塊顯示', verbose_name='Trending')),
('banner_image', models.ForeignKey(blank=True, help_text='文章內文橫幅圖片', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image')),
('cover_image', models.ForeignKey(blank=True, help_text='列表封面圖', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image')),
],
options={
'abstract': False,
},
bases=('wagtailcore.page',),
),
migrations.CreateModel(
name='ArticlePageTag',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content_object', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='tagged_items', to='home.articlepage')),
('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_items', to='taggit.tag')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='articlepage',
name='tags',
field=modelcluster.contrib.taggit.ClusterTaggableManager(blank=True, help_text='A comma-separated list of tags.', through='home.ArticlePageTag', to='taggit.Tag', verbose_name='Tags'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.2.7 on 2026-01-09 05:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0003_categorypage_latestpage_trendingpage_articlepage_and_more'),
]
operations = [
migrations.AddField(
model_name='articlepage',
name='not_news',
field=models.BooleanField(default=False, help_text='不列入最新消息區塊', verbose_name='Not News'),
),
]

View File

@ -1,336 +1,7 @@
import os
from django.db import models from django.db import models
from wagtail.models import Page from wagtail.models import Page
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from modelcluster.contrib.taggit import ClusterTaggableManager
from modelcluster.fields import ParentalKey
from taggit.models import TaggedItemBase
from wagtail.search import index
def _get_env_int(name, default):
value = os.environ.get(name)
if value is None:
return default
try:
return int(value)
except ValueError:
return default
NEWS_SIZE = _get_env_int("HOMEPAGE_NEWS_SIZE", 8) # Default to 8 articles in news layouts
BLOCK_SIZE = _get_env_int("HOMEPAGE_BLOCK_SIZE", 7) # Default to 7 articles in block 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
# Mixin for Category-related functionality
class CategoryMixin:
# Build category blocks
def build_category_blocks(self, request=None):
blocks = []
subcategories = self.get_children().type(CategoryPage).live()
if subcategories.exists():
# If there are subcategories, create blocks for each
for category in subcategories:
blocks.append(
{
"title": category.title,
"items": ArticlePage.objects.child_of(category)
.live()
.order_by("-date", "-id")[:BLOCK_SIZE],
"url": category.url,
"layout": "block",
}
)
else:
# If no subcategories, paginate articles under this category
paginator = Paginator(
ArticlePage.objects.child_of(self)
.live()
.order_by("-date", "-id"),
PAGE_SIZE,
)
page_number = request.GET.get("page") if request else None
try:
page_obj = paginator.page(page_number)
except PageNotAnInteger:
page_obj = paginator.page(1)
except EmptyPage:
page_obj = paginator.page(paginator.num_pages)
blocks.append(
{
"title": self.title,
"items": page_obj,
"url": self.url,
}
)
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
def get_latest_articles(self, request=None):
latest_page = LatestPage.objects.first()
if not request:
# No request means no pagination (e.g., homepage)
return {
"title": latest_page.title,
"items": ArticlePage.objects.filter(not_news=False).live().order_by("-date", "-id")[
:NEWS_SIZE
],
"url": latest_page.url,
}
else:
# Paginated view
paginator = Paginator(
ArticlePage.objects.live().order_by("-date", "-id"), PAGE_SIZE
)
page_number = request.GET.get("page")
try:
page_obj = paginator.page(page_number)
except PageNotAnInteger:
page_obj = paginator.page(1)
except EmptyPage:
page_obj = paginator.page(paginator.num_pages)
return {
"title": self.title,
"items": page_obj,
"url": self.url,
}
def get_trending_articles(self, request=None, exclude_ids=None):
trending_page = TrendingPage.objects.first()
articles_qs = ArticlePage.objects.filter(trending=True).live().order_by(
"-date", "-id"
)
# Exclude specified article IDs
if exclude_ids:
articles_qs = articles_qs.exclude(id__in=exclude_ids)
if not request:
# No request means no pagination (e.g., homepage)
return {
"title": trending_page.title,
"items": articles_qs[:HORIZON_SIZE],
"url": trending_page.url,
}
else:
# Paginated view
paginator = Paginator(articles_qs, PAGE_SIZE)
page_number = request.GET.get("page")
try:
page_obj = paginator.page(page_number)
except PageNotAnInteger:
page_obj = paginator.page(1)
except EmptyPage:
page_obj = paginator.page(paginator.num_pages)
return {
"title": self.title,
"items": page_obj,
"url": self.url,
}
class HomePage(Page, CategoryMixin): class HomePage(Page):
def get_context(self, request): pass
context = super().get_context(request)
sections = {
"top_section": [],
"category_sections": [],
}
latest_section = self.get_latest_articles().copy()
# latest_section["layout"] = "block"
sections["top_section"].append(latest_section)
# NOT NEED TRENDING SECTION CURRENTLY
# ------------------------------------------------------------------
# Exclude latest articles from trending section
# latest_items = latest_section.get("items", [])
# if hasattr(latest_items, "values_list"):
# latest_ids = list(latest_items.values_list("id", flat=True))
# else:
# latest_ids = [item.id for item in latest_items]
# trending_section = self.get_trending_articles(
# exclude_ids=latest_ids
# ).copy()
# trending_section["layout"] = "horizon"
# sections["top_section"].append(trending_section)
# ------------------------------------------------------------------
# Build category sections
categories = CategoryPage.objects.child_of(self).live().in_menu()
for category in categories:
sections["category_sections"].append(
{
"title": category.title,
"url": category.url,
"items": ArticlePage.objects.descendant_of(category)
.live()
.order_by("-date", "-id")[:HORIZON_SIZE],
"layout": "horizon",
}
)
context["sections"] = sections
return context
class LatestPage(Page, CategoryMixin):
template = "home/category_page.html"
def get_context(self, request):
context = super().get_context(request)
context["category_sections"] = [self.get_latest_articles(request)]
breadcrumbs, site_root = self.build_breadcrumbs()
context["breadcrumbs"] = breadcrumbs
context["breadcrumb_root"] = site_root
return context
class TrendingPage(Page, CategoryMixin):
template = "home/category_page.html"
def get_context(self, request):
context = super().get_context(request)
context["category_sections"] = [self.get_trending_articles(request)]
breadcrumbs, site_root = self.build_breadcrumbs()
context["breadcrumbs"] = breadcrumbs
context["breadcrumb_root"] = site_root
return context
class CategoryPage(Page, CategoryMixin):
@property
def has_subcategories(self):
return self.get_children().type(CategoryPage).live().exists()
def get_context(self, request):
context = super().get_context(request)
context["category_sections"] = self.build_category_blocks(request)
# breadcrumbs, site_root = self.build_breadcrumbs()
# context["breadcrumbs"] = breadcrumbs
# context["breadcrumb_root"] = site_root
return context
from wagtail.admin.panels import FieldPanel
from wagtail import blocks
from wagtail.images.blocks import ImageChooserBlock
from wagtail.fields import StreamField
from .blocks import ValidatingEmbedBlock, H2HeadingBlock, HorizontalRuleBlock
# HashTag for Article
class ArticlePageTag(TaggedItemBase):
content_object = ParentalKey(
"home.ArticlePage",
related_name="tagged_items",
on_delete=models.CASCADE,
)
class ArticlePage(Page):
cover_image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
help_text="列表封面圖",
)
banner_image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
help_text="文章內文橫幅圖片",
)
date = models.DateTimeField("Published date")
intro = models.CharField(max_length=250, blank=True)
body = StreamField(
[
("heading", H2HeadingBlock(form_classname="full title")),
("paragraph", blocks.RichTextBlock(features=["bold", "italic", "link"])),
("image", ImageChooserBlock()),
("embed", ValidatingEmbedBlock()),
("hr", HorizontalRuleBlock()),
("html", blocks.RawHTMLBlock(help_text="僅限信任來源的 blockquote/iframe 原始碼")),
],
use_json_field=True,
)
trending = models.BooleanField("Trending", default=False, help_text="在熱門區塊顯示")
not_news = models.BooleanField("Not News", default=False, help_text="不列入最新消息區塊")
tags = ClusterTaggableManager(through="home.ArticlePageTag", blank=True)
search_fields = Page.search_fields + [
index.SearchField("intro", partial_match=True),
index.SearchField("body_search_text", partial_match=True),
index.SearchField("tag_names_search_text", partial_match=True),
]
content_panels = Page.content_panels + [
FieldPanel("trending"),
FieldPanel("not_news"),
FieldPanel("cover_image"),
FieldPanel("banner_image"),
FieldPanel("date"),
FieldPanel("intro"),
FieldPanel("body"),
FieldPanel("tags"),
]
def get_context(self, request):
context = super().get_context(request)
tag_ids = list(self.tags.values_list("id", flat=True))
if tag_ids:
related_articles = (
ArticlePage.objects.live()
.exclude(id=self.id)
.filter(tags__id__in=tag_ids)
.distinct()
.order_by("-date", "-id")[:4]
)
else:
related_articles = ArticlePage.objects.none()
context["related_articles"] = related_articles
return context
@property
def body_search_text(self):
if not self.body:
return ""
excluded_types = {"image", "embed", "hr", "html"}
chunks = []
for block in self.body:
if block.block_type in excluded_types:
continue
# Each block decides how to expose searchable text
block_content = block.block.get_searchable_content(block.value)
if block_content:
chunks.extend(block_content)
return " ".join(text for text in chunks if isinstance(text, str))
@property
def tag_names_search_text(self):
return " ".join(self.tags.values_list("name", flat=True))

View File

@ -1,300 +0,0 @@
.block-list-wrap {
position: relative;
}
.block-first-article {
display: grid;
grid-template-columns: 480px 1fr;
grid-template-areas:
"image content";
gap: 16px 40px;
align-items: start;
}
.block-first-article .first-article-image {
grid-area: image;
position: relative;
border-radius: 10px;
overflow: hidden;
width: 480px;
}
.block-first-article .first-article-image img{
display: block;
width:480px;
height:293px;
object-fit:cover;
border-radius: 10px;
}
.block-first-article .first-article-content {
grid-area: content;
display: flex;
flex-direction: column;
gap: 16px;
max-height: 293px;
overflow: hidden;
position: relative;
}
.block-first-article .first-article-title {
font-size: 40px;
font-weight: 400;
color: #000000;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
flex: 0 0 auto;
}
.block-first-article .first-article-date {
color: #0e1b4266;
flex: 0 0 auto;
}
.block-first-article .first-article-intro {
font-size: 20px;
flex: 0 1 auto;
min-height: 0;
overflow: hidden;
}
.block-first-article .first-article-body {
flex: 1 2 auto;
min-height: 0;
overflow: hidden;
position: relative;
}
.block-first-article .first-article-content::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 100px;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0), #ffffff);
pointer-events: none;
}
.block-list-lower {
margin: 24px 0;
display: flex;
flex-wrap: wrap;
align-items: stretch;
gap: 28px;
}
.block-list-items {
display: contents;
}
.block-list-items a {
display: flex;
flex-direction: column;
gap: 12px;
width: 200px;
text-decoration: none;
}
.block-list-thumb {
position: relative;
width: 194px;
height: 133px;
border-radius: 10px;
overflow: hidden;
}
.block-list-thumb img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.block-list-items a span{
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.block-list-items .article-title {
font-size: 20px;
}
.block-list-items .article-date {
color: #0e1b4266;
}
.block-list-more-article {
max-width: 430px;
min-width: 200px;
flex: 1 1 0;
display: flex;
align-items: center;
justify-content: center;
align-self: stretch;
margin-left: auto;
text-align: center;
}
.block-list-more-article a {
display: flex;
align-items: center;
gap: 12px;
justify-content: center;
}
.block-list-more-article span {
font-size: 18px;
font-weight: 700;
color: #0e1b42;
text-decoration: underline;
}
.block-list-more-article .right-arrow {
background: none;
width: 52px;
height: 52px;
border: none;
padding: 0;
cursor: pointer;
box-shadow: #0e1b4266 1px 2px 1px 1px;
}
.block-list-more-article .right-arrow .right-arrow-icon {
width: 16px;
height: 32px;
transform: rotate(180deg);
}
@media (min-width: 768px) and (max-width: 1023px) {
.block-first-article {
grid-template-columns: 318px 1fr;
}
.block-first-article .first-article-image {
width: 318px;
}
.block-first-article .first-article-image img{
width:318px;
height:290px;
}
.block-first-article .first-article-title {
font-size: 36px;
}
.block-list-lower {
gap: 20px;
}
.block-list-more-article {
max-width: 200px;
flex: 1 1 200px;
}
}
@media (max-width: 767px) {
.block-first-article {
grid-template-columns: 1fr;
grid-template-areas:
"image"
"content";
}
}
@media (min-width: 575px) and (max-width: 767px) {
.block-first-article .first-article-image {
width: 426px;
}
.block-first-article .first-article-image img{
width:426px;
height:260px;
}
.block-first-article .first-article-title {
font-size: 36px;
}
.block-list-items a {
display: grid;
grid-template-columns: 194px 1fr;
column-gap: 16px;
row-gap: 8px;
width: 100%;
}
.block-list-items a > div:nth-of-type(1) {
grid-row: 1 / span 2;
}
.block-list-items a > div:nth-of-type(2),
.block-list-items a > div:nth-of-type(3) {
grid-column: 2;
min-width: 0;
}
.block-list-more-article {
max-width: 100%;
}
}
@media (max-width: 574px) {
.block-first-article .first-article-image {
width: 300px;
}
.block-first-article .first-article-image img{
width: 300px;
height: 220px;
}
.block-first-article .first-article-title {
font-size: 24px;
}
.block-first-article .first-article-intro {
font-size: 16px;
}
.block-list-lower {
gap: 16px;
}
.block-list-items a {
width: 142px;
}
.block-list-thumb {
width: 139px;
height: 110px;
}
.block-list-items .article-title {
font-size: 16px;
}
.block-list-items .article-date {
font-size: 12px;
}
.block-list-more-article span {
font-size: 16px;
}
.block-list-more-article .right-arrow {
width: 40px;
height: 40px;
}
.block-list-more-article .right-arrow .right-arrow-icon {
width: 12px;
height: 24px;
}
}

View File

@ -1,70 +0,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;
}
.category-title {
background-color: #00abf5;
color: #ffffff;
}
.subcategory-title {
display: flex;
align-items: center;
margin: 24px 0;
color: #0e1b42;
font-size: 18px;
font-weight: 700;
}
.subcategory-title::before,
.subcategory-title::after {
content: "";
flex: 1 1 auto;
height: 1px;
background: #0e1b42;
}
.subcategory-title span {
display: inline-block;
width: 190px;
height: 40px;
line-height: 40px;
text-align: center;
border: 1px solid #0e1b42;
background: #ffffff;
}
@media (max-width: 767px) {
.subcategory-title::after {
display: none;
}
}
@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 {
width: 139px;
height: 55px;
line-height: 55px;
}
}

View File

@ -1,108 +0,0 @@
.home-hero-band {
background-color: #0e1b42;
color: #ffffff;
padding-bottom: 86px;
}
.home-hero-band a {
color: #ffffff;
}
.home-banner img {
width: 100%;
height: auto;
display: block;
}
.list-title {
align-items: center;
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 {
display: inline-flex;
width: 28px;
height: 1px;
transform: translate(-4px, -4px);
}
.more-link {
text-decoration: none;
font-size: 16px;
}
.section-b .list-title {
color: #00abf5;
}
.section-b .list-title .category-title {
background-color: #00abf5;
color: #ffffff;
}
.section-b .list-title a{
color: #00abf5;
}
.section-o .list-title {
color: #f4a41c;
}
.section-o .list-title .category-title {
background-color: #f4a41c;
color: #ffffff;
}
.section-o .list-title a{
color: #f4a41c;
}
@media (min-width: 768px) and (max-width: 1023px) {
.list-title .category-title {
width: 142px;
}
}
@media (max-width: 767px) {
.home-banner {
width: 100vw;
margin-left: calc(50% - 50vw);
}
}
@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 {
width: 20px;
height: 1px;
transform: translate(-4px, -4px);
}
.more-link {
font-size: 14px;
}
}

View File

@ -1,232 +0,0 @@
.horizontal-list-wrap {
position: relative;
}
.horizontal-list-arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
width: 52px;
height: 52px;
border: 0;
border-radius: 8px;
background: rgba(255, 255, 255, 0.85);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.18);
cursor: pointer;
}
.horizontal-list-arrow[data-dir="left"] {
left: 0;
}
.horizontal-list-arrow[data-dir="right"] {
right: 0;
}
.horizontal-list-arrow-icon {
width: 18px;
height: 34px;
display: block;
}
.horizontal-list-arrow[data-dir="right"] .horizontal-list-arrow-icon {
transform: rotate(180deg);
}
.horizontal-list-arrow.is-hidden {
opacity: 0;
pointer-events: none;
}
.horizontal-list {
display: flex;
align-items: stretch;
gap: 30px;
overflow-x: auto;
scroll-snap-type: x mandatory;
list-style: none;
margin: 24px 0;
padding: 0;
scrollbar-width: none;
-ms-overflow-style: none;
}
.horizontal-list::-webkit-scrollbar {
display: none;
}
.horizontal-list li {
flex: 0 0 200px;
overflow: hidden;
scroll-snap-align: start;
}
.horizontal-list li a {
display: flex;
flex-direction: column;
gap: 12px;
color: inherit;
text-decoration: none;
}
.horizontal-list-thumb {
position: relative;
width: 194px;
height: 133px;
border-radius: 10px;
overflow: hidden;
}
.horizontal-list-thumb::after {
content: "";
position: absolute;
left: 0px;
right: 0px;
bottom: 0px;
height: 25px;
pointer-events: none;
}
.section-b .horizontal-list-thumb::after {
background: url("../img/picfrm_b194.png") no-repeat left bottom / cover;
background-size: 194px 25px;
}
.section-o .horizontal-list-thumb::after {
background: url("../img/picfrm_o194.png") no-repeat left bottom / cover;
background-size: 194px 25px;
}
.horizontal-list li img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.horizontal-list li a:hover,
.horizontal-list li a:focus {
transform: translateY(-2px);
}
.horizontal-list li a span {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.horizontal-list .article-title {
font-size: 20px;
}
.horizontal-list .article-intro {
font-size: 16px;
}
.horizontal-list .article-date {
color: #0e1b4266;
font-size: 16px;
}
.horizontal-list .empty {
flex: 1 0 auto;
text-align: center;
padding: 24px;
color: #666;
background: #fafafa;
border: 1px dashed #ddd;
border-radius: 12px;
}
@media (min-width: 768px) and (max-width: 1023px), (max-width: 574px) {
.horizontal-list {
gap: 16px;
}
.horizontal-list-thumb {
width: 139px;
height: 110px;
}
.section-b .horizontal-list-thumb::after {
background: url("../img/picfrm_b139.png") no-repeat left bottom / cover;
background-size: 139px 25px;
}
.section-o .horizontal-list-thumb::after {
background: url("../img/picfrm_o139.png") no-repeat left bottom / cover;
background-size: 139px 25px;
}
.horizontal-list a > div:nth-of-type(3) {
display: none;
}
}
@media (min-width: 768px) and (max-width: 1023px){
.horizontal-list li {
flex: 0 0 145px;
}
.horizontal-list .article-title {
width: 145px;
}
}
@media (min-width: 575px) and (max-width: 767px) {
.horizontal-list {
flex-direction: column;
gap: 16px;
overflow-x: visible;
}
.horizontal-list li {
flex: 0 0 auto;
}
.horizontal-list li a {
display: grid;
grid-template-columns: 200px 1fr;
column-gap: 16px;
row-gap: 6px;
}
.horizontal-list li a > div:nth-of-type(1) {
grid-row: 1 / span 3;
}
.horizontal-list li a > div:nth-of-type(2),
.horizontal-list li a > div:nth-of-type(3),
.horizontal-list li a > div:nth-of-type(4) {
grid-column: 2;
}
}
@media (max-width: 574px) {
.horizontal-list-arrow {
display: none;
}
.horizontal-list {
flex-wrap: wrap;
overflow-x: visible;
scroll-snap-type: none;
}
.horizontal-list li {
flex: 0 0 142px;
}
.horizontal-list .article-title {
font-size: 16px;
}
.horizontal-list .article-date {
font-size: 12px;
}
}

View File

@ -1,416 +0,0 @@
.news-title, .more-news-title {
background-color: #ffffff;
color: #0e1b42;
}
.news-hero {
display: grid;
grid-template-columns: 480px 1fr;
grid-template-areas:
"header header"
"image content";
gap: 16px 40px;
align-items: start;
}
.news-hero .news-hero-header {
grid-area: header;
display: flex;
align-items: center;
gap: 12px;
}
.news-hero .list-title {
grid-area: title;
max-width: 310px;
flex: 0 1 310px;
display: block;
}
.news-hero .fist-news-title {
grid-area: head;
max-width: 580px;
font-size: 40px;
font-weight: 400;
color: #eb9f13;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.news-hero .first-news-image {
grid-area: image;
position: relative;
border-radius: 10px;
overflow: hidden;
width: 480px;
}
.news-hero .first-news-image::after {
content: "";
position: absolute;
left: 0px;
right: 0px;
bottom: 0px;
height: 25px;
background: url("../img/picfrm_b480.png") no-repeat left bottom / cover;
background-size: 480px 25px;
pointer-events: none;
}
.news-hero .first-news-image img {
display: block;
width:480px;
height:293px;
object-fit:cover;
border-radius: 10px;
}
.news-hero .first-news-content {
grid-area: content;
display: flex;
flex-direction: column;
gap: 16px;
max-height: 293px;
}
.news-hero .fist-news-date,
.news-hero .first-news-intro {
flex: 0 0 auto;
}
.news-hero .fist-news-date {
color: rgba(255, 255, 255, 0.4);
}
.news-hero .first-news-intro {
font-size: 20px;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.news-hero .first-news-body {
font-size: 13px;
flex: 1 1 auto;
overflow: hidden;
position: relative;
}
.news-hero .first-news-body::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 100px;
background: linear-gradient(to bottom, rgba(14, 27, 66, 0), #0e1b42);
pointer-events: none;
}
.news-list-items {
display: contents;
}
.news-list-lower {
margin-top: 24px;
display: flex;
flex-wrap: wrap;
gap: 28px;
}
.news-list-items a {
display: flex;
flex-direction: column;
gap: 12px;
width: 200px;
text-decoration: none;
}
.news-list-thumb {
position: relative;
width: 194px;
height: 133px;
border-radius: 10px;
overflow: hidden;
}
.news-list-thumb::after {
content: "";
position: absolute;
left: 0px;
right: 0px;
bottom: 0px;
height: 25px;
background: url("../img/picfrm_o194.png") no-repeat left bottom / cover;
background-size: 194px 25px;
pointer-events: none;
}
.news-list-thumb img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.news-list-items a span{
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.news-list-items .article-title {
font-size: 20px;
}
.news-list-items .article-date {
color: rgba(255, 255, 255, 0.4);
}
.more-news {
flex: 1 1 200px;
display: flex;
flex-direction: column;
gap: 16px;
}
.more-news .article-title {
max-width: 200px;
font-size: 20px;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
@media (min-width: 768px) and (max-width: 1023px) {
.news-hero {
grid-template-columns: 318px 1fr;
}
.news-hero .fist-news-title {
max-width: 320px;
}
.news-hero .first-news-image {
width: 318px;
}
.news-hero .first-news-image::after {
background-size: 318px 25px;
}
.news-hero .first-news-image img {
width: 318px;
height: 290px;
}
.news-hero .first-news-content {
height: 290px;
}
.news-list-lower {
gap: 16px;
}
}
@media (min-width: 575px) and (max-width: 1023px) {
.news-hero .fist-news-title {
font-size: 36px;
}
.more-news {
display: grid;
grid-template-columns: 200px 1fr 1fr;
gap: 8px 16px;
align-items: start;
}
.more-news .more-news-title {
grid-column: 1;
grid-row: 1 / span 2;
align-self: stretch;
}
.more-news .article-title {
max-width: none;
}
.more-news a:nth-of-type(1) {
grid-column: 2;
grid-row: 1;
}
.more-news a:nth-of-type(2) {
grid-column: 2;
grid-row: 2;
}
.more-news a:nth-of-type(3) {
grid-column: 3;
grid-row: 1;
}
.more-news a:nth-of-type(4) {
grid-column: 3;
grid-row: 2;
}
}
@media (max-width: 767px) {
.news-hero {
grid-template-columns: 1fr;
grid-template-areas:
"title"
"image"
"head"
"content";
}
.news-hero .news-hero-header {
display: contents;
}
.news-hero .list-title {
max-width: none;
}
}
@media (min-width: 575px) and (max-width: 767px) {
.news-list-items a {
display: grid;
grid-template-columns: 194px 1fr;
column-gap: 16px;
row-gap: 8px;
width: 100%;
}
.news-hero .first-news-image {
width: 426px;
}
.news-hero .first-news-image::after {
background: url("../img/picfrm_b426.png") no-repeat left bottom / cover;
background-size: 426px 25px;
}
.news-hero .first-news-image img {
width:426px;
height:260px;
}
.news-list-items a > div:nth-of-type(1) {
grid-row: 1 / span 3;
}
.news-list-items a > div:nth-of-type(2),
.news-list-items a > div:nth-of-type(3),
.news-list-items a > div:nth-of-type(4) {
grid-column: 2;
}
.more-news {
grid-template-columns: 1fr 1fr;
}
.more-news .more-news-title {
margin-bottom: 20px;
grid-column: 1 / -1;
grid-row: 1;
}
.more-news .article-title {
margin-top: 10px;
}
.more-news a:nth-of-type(1) {
grid-column: 1;
grid-row: 2;
}
.more-news a:nth-of-type(2) {
grid-column: 2;
grid-row: 2;
}
.more-news a:nth-of-type(3) {
grid-column: 1;
grid-row: 3;
}
.more-news a:nth-of-type(4) {
grid-column: 2;
grid-row: 3;
}
}
@media (max-width: 574px) {
.news-hero .first-news-image {
width: 300px;
height: 220px;
justify-self: center;
}
.news-hero .first-news-image::after {
background: url("../img/picfrm_b300.png") no-repeat left bottom / cover;
background-size: 300px 25px;
}
.news-hero .first-news-image img {
width: 300px;
height: 220px;
}
.news-hero .fist-news-title {
font-size: 24px;
}
.news-hero .first-news-intro {
font-size: 16px;
}
.news-list-lower {
gap: 16px;
}
.news-list-items a {
width: 142px;
}
.news-list-thumb {
width: 139px;
height: 110px;
}
.news-list-thumb::after {
background: url("../img/picfrm_o139.png") no-repeat left bottom / cover;
background-size: 139px 25px;
}
.news-list-items a > div:nth-of-type(3) {
display: none;
}
.news-list-items .article-title {
font-size: 16px;
}
.news-list-items .article-date {
font-size: 12px;
}
.more-news{
max-width: 142px;
gap: 12px;
}
.more-news .article-title {
font-size: 16px;
}
}

View File

@ -0,0 +1,184 @@
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
max-width: 960px;
min-height: 100vh;
margin: 0 auto;
padding: 0 15px;
color: #231f20;
font-family: 'Helvetica Neue', 'Segoe UI', Arial, sans-serif;
line-height: 1.25;
}
a {
background-color: transparent;
color: #308282;
text-decoration: underline;
}
a:hover {
color: #ea1b10;
}
h1,
h2,
h3,
h4,
h5,
p,
ul {
padding: 0;
margin: 0;
font-weight: 400;
}
svg:not(:root) {
overflow: hidden;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #e6e6e6;
}
.logo {
width: 150px;
margin-inline-end: 20px;
}
.logo a {
display: block;
}
.figure-logo {
max-width: 150px;
max-height: 55.1px;
}
.release-notes {
font-size: 14px;
}
.main {
padding: 40px 0;
margin: 0 auto;
text-align: center;
}
.figure-space {
max-width: 265px;
}
@keyframes pos {
0%, 100% {
transform: rotate(-6deg);
}
50% {
transform: rotate(6deg);
}
}
.egg {
fill: #43b1b0;
animation: pos 3s ease infinite;
transform: translateY(50px);
transform-origin: 50% 80%;
}
.main-text {
max-width: 400px;
margin: 5px auto;
}
.main-text h1 {
font-size: 22px;
}
.main-text p {
margin: 15px auto 0;
}
.footer {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
border-top: 1px solid #e6e6e6;
padding: 10px;
}
.option {
display: block;
padding: 10px 10px 10px 34px;
position: relative;
text-decoration: none;
}
.option svg {
width: 24px;
height: 24px;
fill: gray;
border: 1px solid #d9d9d9;
padding: 5px;
border-radius: 100%;
top: 10px;
inset-inline-start: 0;
position: absolute;
}
.option h2 {
font-size: 19px;
text-decoration: underline;
}
.option p {
padding-top: 3px;
color: #231f20;
font-size: 15px;
font-weight: 300;
}
@media (max-width: 996px) {
body {
max-width: 780px;
}
}
@media (max-width: 767px) {
.option {
flex: 0 0 50%;
}
}
@media (max-width: 599px) {
.main {
padding: 20px 0;
}
.figure-space {
max-width: 200px;
}
.footer {
display: block;
width: 300px;
margin: 0 auto;
}
}
@media (max-width: 360px) {
.header-link {
max-width: 100px;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 926 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 938 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1,45 +0,0 @@
(function () {
function initBlockList(block) {
var list = block.querySelector('.block-list');
var left = block.querySelector('[data-dir="left"]');
var right = block.querySelector('[data-dir="right"]');
if (!list || !left || !right) {
return;
}
function getScrollAmount() {
return Math.max(200, Math.floor(list.clientWidth * 0.8));
}
function updateArrows() {
var maxScroll = list.scrollWidth - list.clientWidth;
var hasOverflow = maxScroll > 1;
var atStart = list.scrollLeft <= 1;
var atEnd = list.scrollLeft >= maxScroll - 1;
left.classList.toggle('is-hidden', !hasOverflow || atStart);
right.classList.toggle('is-hidden', !hasOverflow || atEnd);
}
left.addEventListener('click', function () {
list.scrollBy({ left: -getScrollAmount(), behavior: 'smooth' });
});
right.addEventListener('click', function () {
list.scrollBy({ left: getScrollAmount(), behavior: 'smooth' });
});
list.addEventListener('scroll', function () {
window.requestAnimationFrame(updateArrows);
});
window.addEventListener('resize', updateArrows);
updateArrows();
}
document.addEventListener('DOMContentLoaded', function () {
var blocks = document.querySelectorAll('[data-block-list]');
blocks.forEach(initBlockList);
});
})();

View File

@ -1,45 +0,0 @@
(function () {
function initHorizontalList(block) {
var list = block.querySelector('.horizontal-list');
var left = block.querySelector('[data-dir="left"]');
var right = block.querySelector('[data-dir="right"]');
if (!list || !left || !right) {
return;
}
function getScrollAmount() {
return Math.max(200, Math.floor(list.clientWidth * 0.8));
}
function updateArrows() {
var maxScroll = list.scrollWidth - list.clientWidth;
var hasOverflow = maxScroll > 1;
var atStart = list.scrollLeft <= 1;
var atEnd = list.scrollLeft >= maxScroll - 1;
left.classList.toggle('is-hidden', !hasOverflow || atStart);
right.classList.toggle('is-hidden', !hasOverflow || atEnd);
}
left.addEventListener('click', function () {
list.scrollBy({ left: -getScrollAmount(), behavior: 'smooth' });
});
right.addEventListener('click', function () {
list.scrollBy({ left: getScrollAmount(), behavior: 'smooth' });
});
list.addEventListener('scroll', function () {
window.requestAnimationFrame(updateArrows);
});
window.addEventListener('resize', updateArrows);
updateArrows();
}
document.addEventListener('DOMContentLoaded', function () {
var blocks = document.querySelectorAll('[data-horizontal-list]');
blocks.forEach(initHorizontalList);
});
})();

View File

@ -1,35 +0,0 @@
{% extends "base.html" %}
{% load wagtailcore_tags wagtailimages_tags %}
{% block content %}
<article>
<h1>{{ page.title }}</h1>
{% if page.banner_image %}
{% image page.banner_image original as banner %}
<img src="{{ banner.url }}" alt="{{ page.title }}">
{% endif %}
<p class="date">{{ page.date }}</p>
<div class="intro">{{ page.intro }}</div>
<div class="body">
{{ page.body }}
</div>
{% with tags=page.tags.all %}
{% if tags %}
<div class="tags">
<span>Hashtags:</span>
<ul>
{% for tag in tags %}
<li><a href="{% url 'hashtag_search' tag.slug %}">#{{ tag }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endwith %}
{% if related_articles %}
<section class="related-articles">
<h2>相關文章</h2>
{% include "home/includes/article_list.html" with items=related_articles %}
</section>
{% endif %}
</article>
{% endblock %}

View File

@ -1 +0,0 @@
<h2 class="article-heading">{{ value }}</h2>

View File

@ -1 +0,0 @@
<hr class="article-hr">

View File

@ -1,37 +0,0 @@
{% extends "base.html" %}
{% load wagtailcore_tags static %}
{% block extra_css %}
<link rel="stylesheet" type="text/css" href="{% static 'css/category.css' %}">
{% endblock %}
{% block content %}
{# Breadcrumbs navigation may not used anymore #}
{% if breadcrumbs %}
<nav class="breadcrumbs" aria-label="breadcrumb">
<ol>
{% if breadcrumb_root %}
<li><a href="{{ breadcrumb_root.url }}">首頁</a></li>
{% endif %}
{% for crumb in breadcrumbs %}
{% if not breadcrumb_root or crumb.id != breadcrumb_root.id %}
<li>
{% if crumb.id == page.id %}
<span>{{ crumb.title }}</span>
{% else %}
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
{% endif %}
</li>
{% endif %}
{% endfor %}
</ol>
</nav>
{% endif %}
<div class="block-title category-title"><span>{{ self.title}}</span></div>
{% if page.has_subcategories %}
{% for section in category_sections %}
<div class="subcategory-title"><span>{{ section.title }}</span></div>
{% include "home/includes/block_list.html" with items=section.items %}
{% endfor %}
{% else %}
{% include "home/includes/page-article-list.html" with category=category_sections.0 %}
{% endif %}
{% endblock %}

View File

@ -1,19 +0,0 @@
{% extends "base.html" %}
{% load wagtailcore_tags %}
{% block content %}
<nav class="breadcrumbs" aria-label="breadcrumb">
<ol>
<li>
{% if site_root %}
<a href="{{ site_root.url }}">首頁</a>
{% else %}
<a href="/">首頁</a>
{% endif %}
</li>
<li><span>標籤</span></li>
<li><span>#{{ tag.name }}</span></li>
</ol>
</nav>
{% include "home/includes/page-article-list.html" %}
{% endblock %}

View File

@ -1,32 +1,21 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% block body_class %}template-darkbackground{% endblock %}
{% block extra_css %}
<link rel="stylesheet" type="text/css" href="{% static 'css/home.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/news_list.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/block_list.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/horizontal_list.css' %}">
{% endblock %}
{% block content %}
<div class="home-hero-band full-bleed">
<div class="site-container">
<div class="home-hero">
{% include "base/includes/home_banner.html" %}
{% with top_section=sections.top_section %}
{% for section in top_section %}
{% include "home/includes/news_list.html" with section=section %}
{% endfor %}
{% endwith %}
</div>
</div>
</div>
{% for section in sections.category_sections %} {% block body_class %}template-homepage{% endblock %}
{% cycle 'section-b' 'section-o' as section_color silent %}
{% include "home/includes/category_session.html" with section=section section_color=section_color %} {% block extra_css %}
{% endfor %}
{% comment %}
Delete the line below if you're just getting started and want to remove the welcome screen!
{% endcomment %}
<link rel="stylesheet" href="{% static 'css/welcome_page.css' %}">
{% endblock extra_css %}
{% block content %}
{% comment %}
Delete the line below if you're just getting started and want to remove the welcome screen!
{% endcomment %}
{% include 'home/welcome_page.html' %}
{% endblock content %} {% endblock content %}
{% block extra_js %}
<script type="text/javascript" src="{% static 'js/block_list.js' %}"></script>
<script type="text/javascript" src="{% static 'js/horizontal_list.js' %}"></script>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% load wagtailimages_tags static %}
<ul class="article-list">
{% for article in items %}
<li>
<article>
<a href="{{ article.url }}">
{% if article.cover_image %}
{% image article.cover_image max-200x200 as cover %}
<img src="{{ cover.url }}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
{% else %}
<img src="{% static 'img/default_cover.jpg' %}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
{% endif %}
{{ article.title }}
</a>
</article>
</li>
{% empty %}
<li class="empty">目前沒有文章</li>
{% endfor %}
</ul>

View File

@ -1,74 +0,0 @@
{% load wagtailimages_tags static %}
{% block extra_css %}
<link rel="stylesheet" type="text/css" href="{% static 'css/block_list.css' %}">
{% endblock %}
<div class="block-list-wrap" data-block-list>
{% with first_article=items|first %}
{% if first_article %}
<div class="block-first-article">
<div class="first-article-image">
<a href="{{ first_article.url }}">
{% if first_article.cover_image %}
{% image first_article.cover_image max-480x293 as cover %}
<img src="{{ cover.url }}" alt="{{ first_article.title }}"/>
{% else %}
<img src="{% static 'img/default_cover.jpg' %}" alt="{{ first_article.title }}"/>
{% endif %}
</a>
</div>
<div class="first-article-content">
<div class="first-article-title">
<span>{{ first_article.title }}</span>
</div>
<div class="first-article-date">
<span>{{ first_article.date|date:"Y.m.d" }}</span>
</div>
{% if first_article.intro %}
<div class="first-article-intro">
<span>{{ first_article.intro }}</span>
</div>
{% endif %}
<div class="first-article-body">
<span>{{ first_article.body_search_text|truncatechars:200 }}</span>
</div>
</div>
</div>
{% else %}
<span class="empty">目前沒有文章</span>
{% endif %}
{% endwith %}
{% if items|length >= 2 %}
<!-- <div class="list-title">
<span class="more-link"><a href="{{ items.0.get_parent.url }}">查看全部</a></span> -->
<div class="block-list-lower">
<div class="block-list-items">
{% for article in items|slice:"1:7" %}
<a href="{{ article.url }}">
<div class="block-list-thumb">
{% if article.cover_image %}
{% image article.cover_image max-194x133 as cover %}
<img src="{{ cover.url }}" alt="{{ article.title }}"/>
{% else %}
<img src="{% static 'img/default_cover.jpg' %}" alt="{{ article.title }}"/>
{% endif %}
</div>
<div><span class="article-title">{{ article.title }}</span></div>
<div><span class="article-date">{{ article.date|date:"Y.m.d" }}</span></div>
</a>
{% endfor %}
</div>
<div class="block-list-more-article">
<a href="{{ items.0.get_parent.url }}">
<span>更多文章</span>
<button class="right-arrow" type="button" data-dir="right" aria-label="更多文章">
<svg class="right-arrow-icon" xmlns="http://www.w3.org/2000/svg" fill="none" overflow="visible" preserveAspectRatio="none" viewBox="0 0 18.1213 33.4142">
<path d="M17.4142 0.707107L1.41421 16.7071L17.4142 32.7071" id="Vector 3" stroke="var(--stroke-0, #0E1B42)" stroke-width="2"/>
</svg>
</button>
</a>
</div>
</div>
{% endif %}
</div>

View File

@ -1,19 +0,0 @@
{% load wagtailimages_tags static %}
<section class="article-section article-section--{{ section.layout }} {{ section_color }}">
<div class="list-title {{ title_variant }}">
<div class="block-title category-title"><span>{{ section.title }}</span></div>
<span class="block-title-divider" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" height="100%" overflow="visible" preserveAspectRatio="none" viewBox="0 0 28 1" width="100%">
<line stroke="currentColor" x2="28" y1="0.5" y2="0.5"/>
</svg>
</span>
<a href="{{ section.url }}"><span class="more-link">查看全部</span></a>
</div>
{% if section.layout == "block" %}
{% include "home/includes/block_list.html" with items=section.items %}
{% elif section.layout == "horizon" %}
{% include "home/includes/horizontal_list.html" with items=section.items %}
{% endif %}
</section>

View File

@ -1,35 +0,0 @@
{% load wagtailimages_tags static %}
<div class="horizontal-list-wrap" data-horizontal-list>
<button class="horizontal-list-arrow is-hidden" type="button" data-dir="left" aria-label="上一頁">
<svg class="horizontal-list-arrow-icon" xmlns="http://www.w3.org/2000/svg" fill="none" overflow="visible" preserveAspectRatio="none" viewBox="0 0 18.1213 33.4142">
<path d="M17.4142 0.707107L1.41421 16.7071L17.4142 32.7071" id="Vector 3" stroke="var(--stroke-0, #0E1B42)" stroke-width="2"/>
</svg>
</button>
<ul class="horizontal-list">
{% for article in items %}
<li>
<a href="{{ article.url }}">
<div class="horizontal-list-thumb">
{% if article.cover_image %}
{% image article.cover_image max-194x133 as cover %}
<img src="{{ cover.url }}" alt="{{ article.title }}" height="133" width="194"/>
{% else %}
<img src="{% static 'img/default_cover.jpg' %}" alt="{{ article.title }}" height="133" width="194"/>
{% endif %}
</div>
<div><span class="article-title">{{ article.title }}</span></div>
<div><span class="article-intro">{{ article.intro }}</span></div>
<div><span class="article-date">{{ article.date|date:"Y.m.d" }}</span></div>
</a>
</li>
{% empty %}
<li class="empty">目前沒有文章</li>
{% endfor %}
</ul>
<button class="horizontal-list-arrow" type="button" data-dir="right" aria-label="下一頁">
<svg class="horizontal-list-arrow-icon" xmlns="http://www.w3.org/2000/svg" fill="none" overflow="visible" preserveAspectRatio="none" viewBox="0 0 18.1213 33.4142">
<path d="M17.4142 0.707107L1.41421 16.7071L17.4142 32.7071" id="Vector 3" stroke="var(--stroke-0, #0E1B42)" stroke-width="2"/>
</svg>
</button>
</div>

View File

@ -1,84 +0,0 @@
{% load wagtailimages_tags static %}
<div class="news-list-wrap" data-news-list>
<div class="news-hero">
<div class="news-hero-header">
<div class="list-title">
<div class="block-title news-title"><span>本日頭條</span></div>
<span class="block-title-divider" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" height="100%" overflow="visible" preserveAspectRatio="none" viewBox="0 0 28 1" width="100%">
<line stroke="currentColor" x2="28" y1="0.5" y2="0.5"/>
</svg>
</span>
<a href="{{ section.url }}"><span class="more-link">查看全部</span></a>
</div>
{% with first_article=section.items|first %}
{% if first_article %}
<div class="fist-news-title">
<span>{{ first_article.title }}</span>
</div>
{% endif %}
{% endwith %}
</div>
{% with first_article=section.items|first %}
{% if first_article %}
<div class="first-news-image">
<a href="{{ first_article.url }}">
{% if first_article.cover_image %}
{% image first_article.cover_image max-480x293 as cover %}
<img src="{{ cover.url }}" alt="{{ first_article.title }}" height="293" width="480"/>
{% else %}
<img src="{% static 'img/default_cover.jpg' %}" alt="{{ first_article.title }}" height="293" width="480"/>
{% endif %}
</a>
</div>
<div class="first-news-content">
<div class="fist-news-date">
<span>{{ first_article.date|date:"Y.m.d" }}</span>
</div>
{% if first_article.intro %}
<div class="first-news-intro">
<span>{{ first_article.intro }}</span>
</div>
{% endif %}
<div class="first-news-body">
<span>{{ first_article.body_search_text|truncatechars:320 }}</span>
</div>
</div>
{% else %}
<span class="empty">目前沒有文章</span>
{% endif %}
{% endwith %}
</div>
{% if section.items|length >= 2 %}
<div class="news-list-lower">
<div class="news-list-items">
{% for article in section.items|slice:"1:4" %}
<a href="{{ article.url }}">
<div class="news-list-thumb">
{% if article.cover_image %}
{% image article.cover_image max-194x133 as cover %}
<img src="{{ cover.url }}" alt="{{ article.title }}" height="133" width="194"/>
{% else %}
<img src="{% static 'img/default_cover.jpg' %}" alt="{{ article.title }}" height="133" width="194"/>
{% endif %}
</div>
<div><span class="article-title">{{ article.title }}</span></div>
<div><span class="article-intro">{{ article.intro }}</span></div>
<div><span class="article-date">{{ article.date|date:"Y.m.d" }}</span></div>
</a>
{% endfor %}
</div>
<div class="more-news">
<div class="block-title more-news-title"><span>更多頭條</span></div>
{% for article in section.items|slice:"4:8" %}
<a href="{{ article.url }}">
<span class="article-title">{{ article.title }}</span>
</a>
{% endfor %}
</div>
</div>
{% endif %}
</div>

View File

@ -1,22 +0,0 @@
{% load wagtailimages_tags static %}
<div class="page-article-list">
{% with category=category_sections.0 %}
<h2><a href="{{ category.url }}">{{ category.title }}</a></h2>
{% include "home/includes/article_list.html" with items=category.items %}
{% if category.items.paginator.num_pages > 1 %}
<div class="pagination">
{% if category.items.has_previous %}
<a href="?page={{ category.items.previous_page_number }}">上一頁</a>
{% endif %}
<span>第 {{ category.items.number }} / {{ category.items.paginator.num_pages }} 頁</span>
{% if category.items.has_next %}
<a href="?page={{ category.items.next_page_number }}">下一頁</a>
{% endif %}
</div>
{% endif %}
{% endwith %}
</div>

View File

@ -0,0 +1,52 @@
{% load i18n wagtailcore_tags %}
<header class="header">
<div class="logo">
<a href="https://wagtail.org/">
<svg class="figure-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 342.5 126.2"><title>{% trans "Visit the Wagtail website" %}</title><path fill="#FFF" d="M84 1.9v5.7s-10.2-3.8-16.8 3.1c-4.8 5-5.2 10.6-3 18.1 21.6 0 25 12.1 25 12.1L87 27l6.8-8.3c0-9.8-8.1-16.3-9.8-16.8z"/><circle cx="85.9" cy="15.9" r="2.6"/><path d="M89.2 40.9s-3.3-16.6-24.9-12.1c-2.2-7.5-1.8-13 3-18.1C73.8 3.8 84 7.6 84 7.6V1.9C80.4.3 77 0 73.2 0 59.3 0 51.6 10.4 48.3 17.4L9.2 89.3l11-2.1-20.2 39 14.1-2.5L24.9 93c30.6 0 69.8-11 64.3-52.1z"/><path d="M102.4 27l-8.6-8.3L87 27z"/><path fill="#FFF" d="M30 84.1s1-.2 2.8-.6c1.8-.4 4.3-1 7.3-1.8 1.5-.4 3.1-.9 4.8-1.5 1.7-.6 3.5-1.2 5.2-2 1.8-.7 3.6-1.6 5.4-2.6 1.8-1 3.5-2.1 5.1-3.4.4-.3.8-.6 1.2-1l1.2-1c.7-.7 1.5-1.4 2.2-2.2.7-.7 1.3-1.5 1.9-2.3l.9-1.2.4-.6.4-.6c.2-.4.5-.8.7-1.2.2-.4.4-.8.7-1.2l.3-.6.3-.6c.2-.4.4-.8.5-1.2l.9-2.4c.2-.8.5-1.6.7-2.3.2-.7.3-1.5.5-2.1.1-.7.2-1.3.3-2 .1-.6.2-1.2.2-1.7.1-.5.1-1 .2-1.5.1-1.8.1-2.8.1-2.8l1.6.1s-.1 1.1-.2 2.9c-.1.5-.1 1-.2 1.5-.1.6-.1 1.2-.3 1.8-.1.6-.3 1.3-.4 2-.2.7-.4 1.4-.6 2.2-.2.8-.5 1.5-.8 2.4-.3.8-.6 1.6-1 2.5l-.6 1.2-.3.6-.3.6c-.2.4-.5.8-.7 1.3-.3.4-.5.8-.8 1.2-.1.2-.3.4-.4.6l-.4.6-.9 1.2c-.7.8-1.3 1.6-2.1 2.3-.7.8-1.5 1.4-2.3 2.2l-1.2 1c-.4.3-.8.6-1.3.9-1.7 1.2-3.5 2.3-5.3 3.3-1.8.9-3.7 1.8-5.5 2.5-1.8.7-3.6 1.3-5.3 1.8-1.7.5-3.3 1-4.9 1.3-3 .7-5.6 1.3-7.4 1.6-1.6.6-2.6.8-2.6.8z"/><g fill="#231F20"><path d="M127 83.9h-8.8l-12.6-36.4h7.9l9 27.5 9-27.5h7.9l9 27.5 9-27.5h7.9L153 83.9h-8.8L135.6 59 127 83.9zM200.1 83.9h-7V79c-3 3.6-7 5.4-12.1 5.4-3.8 0-6.9-1.1-9.4-3.2s-3.7-5-3.7-8.6c0-3.6 1.3-6.3 4-8 2.6-1.8 6.2-2.7 10.7-2.7h9.9v-1.4c0-4.8-2.7-7.3-8.1-7.3-3.4 0-6.9 1.2-10.5 3.7l-3.4-4.8c4.4-3.5 9.4-5.3 15.1-5.3 4.3 0 7.8 1.1 10.5 3.2 2.7 2.2 4.1 5.6 4.1 10.2v23.7zm-7.7-13.6v-3.1h-8.6c-5.5 0-8.3 1.7-8.3 5.2 0 1.8.7 3.1 2.1 4.1 1.4.9 3.3 1.4 5.7 1.4 2.4 0 4.6-.7 6.4-2.1 1.8-1.3 2.7-3.1 2.7-5.5zM241.7 47.5v31.7c0 6.4-1.7 11.3-5.2 14.5-3.5 3.2-8 4.8-13.4 4.8-5.5 0-10.4-1.7-14.8-5.1l3.6-5.8c3.6 2.7 7.1 4 10.8 4 3.6 0 6.5-.9 8.6-2.8 2.1-1.9 3.2-4.9 3.2-9v-4.7c-1.1 2.1-2.8 3.9-4.9 5.1-2.1 1.3-4.5 1.9-7.1 1.9-4.8 0-8.8-1.7-11.9-5.1-3.1-3.4-4.7-7.6-4.7-12.6s1.6-9.2 4.7-12.6c3.1-3.4 7.1-5.1 11.9-5.1 4.8 0 8.7 2 11.7 6v-5.4h7.5zm-28.4 16.8c0 3 .9 5.6 2.8 7.7 1.8 2.2 4.3 3.2 7.5 3.2 3.1 0 5.7-1 7.6-3.1 1.9-2.1 2.9-4.7 2.9-7.8 0-3.1-1-5.8-2.9-7.9-2-2.2-4.5-3.2-7.6-3.2-3.1 0-5.6 1.1-7.4 3.4-2 2.1-2.9 4.7-2.9 7.7zM260.9 53.6v18.5c0 1.7.5 3.1 1.4 4.1.9 1 2.2 1.5 3.8 1.5 1.6 0 3.2-.8 4.7-2.4l3.1 5.4c-2.7 2.4-5.7 3.6-8.9 3.6-3.3 0-6-1.1-8.3-3.4-2.3-2.3-3.5-5.3-3.5-9.1V53.6h-4.6v-6.2h4.6V36.1h7.7v11.4h9.6v6.2h-9.6zM309.5 83.9h-7V79c-3 3.6-7 5.4-12.1 5.4-3.8 0-6.9-1.1-9.4-3.2s-3.7-5-3.7-8.6c0-3.6 1.3-6.3 4-8 2.6-1.8 6.2-2.7 10.7-2.7h9.9v-1.4c0-4.8-2.7-7.3-8.1-7.3-3.4 0-6.9 1.2-10.5 3.7l-3.4-4.8c4.4-3.5 9.4-5.3 15.1-5.3 4.3 0 7.8 1.1 10.5 3.2 2.7 2.2 4.1 5.6 4.1 10.2v23.7zm-7.7-13.6v-3.1h-8.6c-5.5 0-8.3 1.7-8.3 5.2 0 1.8.7 3.1 2.1 4.1 1.4.9 3.3 1.4 5.7 1.4 2.4 0 4.6-.7 6.4-2.1 1.8-1.3 2.7-3.1 2.7-5.5zM319.3 40.2c-1-1-1.4-2.1-1.4-3.4 0-1.3.5-2.5 1.4-3.4 1-1 2.1-1.4 3.4-1.4 1.3 0 2.5.5 3.4 1.4 1 1 1.4 2.1 1.4 3.4 0 1.3-.5 2.5-1.4 3.4s-2.1 1.4-3.4 1.4c-1.3.1-2.4-.4-3.4-1.4zm7.2 43.7h-7.7V47.5h7.7v36.4zM342.5 83.9h-7.7V33.1h7.7v50.8z"/></g></svg>
</a>
</div>
<div class="header-link">
{% comment %}
This works for all cases but prerelease versions:
{% endcomment %}
<a href="{% wagtail_documentation_path %}/releases/{% wagtail_release_notes_path %}">
{% trans "View the release notes" %}
</a>
</div>
</header>
<main class="main">
<div class="figure">
<svg class="figure-space" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300" aria-hidden="true">
<path class="egg" fill="currentColor" d="M150 250c-42.741 0-75-32.693-75-90s42.913-110 75-110c32.088 0 75 52.693 75 110s-32.258 90-75 90z"/>
<ellipse fill="#ddd" cx="150" cy="270" rx="40" ry="7"/>
</svg>
</div>
<div class="main-text">
<h1>{% trans "Welcome to your new Wagtail site!" %}</h1>
<p>{% trans 'Please feel free to <a href="https://github.com/wagtail/wagtail/wiki/Slack">join our community on Slack</a>, or get started with one of the links below.' %}</p>
</div>
</main>
<footer class="footer" role="contentinfo">
<a class="option option-one" href="{% wagtail_documentation_path %}/">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 21c0 .5.4 1 1 1h4c.6 0 1-.5 1-1v-1H9v1zm3-19C8.1 2 5 5.1 5 9c0 2.4 1.2 4.5 3 5.7V17c0 .5.4 1 1 1h6c.6 0 1-.5 1-1v-2.3c1.8-1.3 3-3.4 3-5.7 0-3.9-3.1-7-7-7zm2.9 11.1l-.9.6V16h-4v-2.3l-.9-.6C7.8 12.2 7 10.6 7 9c0-2.8 2.2-5 5-5s5 2.2 5 5c0 1.6-.8 3.2-2.1 4.1z"/></svg>
<div>
<h2>{% trans "Wagtail Documentation" %}</h2>
<p>{% trans "Topics, references, & how-tos" %}</p>
</div>
</a>
<a class="option option-two" href="{% wagtail_documentation_path %}/getting_started/tutorial.html">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>
<div>
<h2>{% trans "Tutorial" %}</h2>
<p>{% trans "Build your first Wagtail site" %}</p>
</div>
</a>
<a class="option option-three" href="{% url 'wagtailadmin_home' %}">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M0 0h24v24H0z" fill="none"/><path d="M16.5 13c-1.2 0-3.07.34-4.5 1-1.43-.67-3.3-1-4.5-1C5.33 13 1 14.08 1 16.25V19h22v-2.75c0-2.17-4.33-3.25-6.5-3.25zm-4 4.5h-10v-1.25c0-.54 2.56-1.75 5-1.75s5 1.21 5 1.75v1.25zm9 0H14v-1.25c0-.46-.2-.86-.52-1.22.88-.3 1.96-.53 3.02-.53 2.44 0 5 1.21 5 1.75v1.25zM7.5 12c1.93 0 3.5-1.57 3.5-3.5S9.43 5 7.5 5 4 6.57 4 8.5 5.57 12 7.5 12zm0-5.5c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zm9 5.5c1.93 0 3.5-1.57 3.5-3.5S18.43 5 16.5 5 13 6.57 13 8.5s1.57 3.5 3.5 3.5zm0-5.5c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2z"/></svg>
<div>
<h2>{% trans "Admin Interface" %}</h2>
<p>{% trans "Create your superuser first!" %}</p>
</div>
</a>
</footer>

View File

@ -1,44 +0,0 @@
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.shortcuts import get_object_or_404, render
from taggit.models import Tag
from wagtail.models import Site
from .models import ArticlePage, PAGE_SIZE
def hashtag_search(request, slug):
tag = get_object_or_404(Tag, slug=slug)
articles = (
ArticlePage.objects.live()
.filter(tags__slug=slug)
.order_by("-date")
)
paginator = Paginator(articles, PAGE_SIZE)
page_number = request.GET.get("page")
try:
page_obj = paginator.page(page_number)
except PageNotAnInteger:
page_obj = paginator.page(1)
except EmptyPage:
page_obj = paginator.page(paginator.num_pages)
site = Site.find_for_request(request)
site_root = site.root_page if site else None
context = {
"tag": tag,
"category_sections": [
{
"title": f"#{tag.name}",
"items": page_obj,
"url": request.path,
}
],
"site_root": site_root,
"page": site_root.specific if site_root else None,
}
return render(request, "home/hashtag_page.html", context)

View File

@ -3,10 +3,6 @@
import os import os
import sys import sys
from dotenv import load_dotenv
env_file = os.environ.get("ENV_FILE", "../.env")
load_dotenv(env_file)
def main(): def main():
"""Run administrative tasks.""" """Run administrative tasks."""

View File

@ -16,15 +16,6 @@ import os
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BASE_DIR = os.path.dirname(PROJECT_DIR) BASE_DIR = os.path.dirname(PROJECT_DIR)
def env_list(name, default):
"""
Return a list from a comma-separated env var; fall back to provided default list.
"""
value = os.environ.get(name)
if value:
return [item.strip() for item in value.split(",") if item.strip()]
return default
# 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/
@ -37,7 +28,6 @@ INSTALLED_APPS = [
"search", "search",
"wagtail.contrib.forms", "wagtail.contrib.forms",
"wagtail.contrib.redirects", "wagtail.contrib.redirects",
"wagtail.contrib.settings",
"wagtail.embeds", "wagtail.embeds",
"wagtail.sites", "wagtail.sites",
"wagtail.users", "wagtail.users",
@ -56,7 +46,6 @@ INSTALLED_APPS = [
"django.contrib.sessions", "django.contrib.sessions",
"django.contrib.messages", "django.contrib.messages",
"django.contrib.staticfiles", "django.contrib.staticfiles",
"base",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -85,8 +74,6 @@ TEMPLATES = [
"django.template.context_processors.request", "django.template.context_processors.request",
"django.contrib.auth.context_processors.auth", "django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages", "django.contrib.messages.context_processors.messages",
"wagtail.contrib.settings.context_processors.settings",
"home.context_processors.navigation_pages",
], ],
}, },
}, },
@ -134,28 +121,6 @@ USE_I18N = True
USE_TZ = True USE_TZ = True
# --- Wagtail embeds (Instagram/FB via Graph oEmbed) ---
# Reads a token from env and adds an extra oEmbed finder for IG/FB.
# Keeps the default oEmbed finder first for YouTube/Vimeo/etc.
WAGTAIL_EMBED_FINDERS = [
{"class": "wagtail.embeds.finders.oembed"},
{
"class": "wagtail.embeds.finders.oembed",
"options": {
"providers": [
{"endpoint": "https://graph.facebook.com/v11.0/instagram_oembed", "urls": ["https://www.instagram.com/*"]},
{"endpoint": "https://graph.facebook.com/v11.0/oembed_post", "urls": ["https://www.facebook.com/*"]},
{"endpoint": "https://graph.facebook.com/v11.0/oembed_page", "urls": ["https://www.facebook.com/*"]},
{"endpoint": "https://graph.facebook.com/v11.0/oembed_video", "urls": ["https://www.facebook.com/*"]},
],
"params": {
"access_token": os.environ.get("IG_OEMBED_ACCESS_TOKEN", ""),
"omitscript": True,
},
},
},
]
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
LANGUAGES = [ LANGUAGES = [
@ -179,30 +144,19 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static")
STATIC_URL = "/static/" STATIC_URL = "/static/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media") MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = f'{os.environ.get("AWS_S3_ENDPOINT_URL")}/{os.environ.get("AWS_STORAGE_BUCKET_NAME")}/' MEDIA_URL = "/media/"
# Default storage settings # Default storage settings
# See https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STORAGES # See https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STORAGES
STORAGES = { STORAGES = {
"default": { "default": {
"BACKEND": "storages.backends.s3boto3.S3Boto3Storage", "BACKEND": "django.core.files.storage.FileSystemStorage",
"OPTIONS": {
"endpoint_url": os.environ.get("AWS_S3_ENDPOINT_URL"),
"access_key": os.environ.get("AWS_ACCESS_KEY_ID"),
"secret_key": os.environ.get("AWS_SECRET_ACCESS_KEY"),
"bucket_name": os.environ.get("AWS_STORAGE_BUCKET_NAME"),
"region_name": os.environ.get("AWS_S3_REGION_NAME", default="us-east-1"),
"addressing_style": "path",
},
}, },
"staticfiles": { "staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
}, },
} }
# Avoid overwriting user uploads when using S3 storage unless explicitly enabled via env
AWS_S3_FILE_OVERWRITE = os.environ.get("AWS_S3_FILE_OVERWRITE", "False").lower() == "true"
# Django sets a maximum of 1000 fields per form by default, but particularly complex page models # Django sets a maximum of 1000 fields per form by default, but particularly complex page models
# can exceed this limit within Wagtail's page editor. # can exceed this limit within Wagtail's page editor.
DATA_UPLOAD_MAX_NUMBER_FIELDS = 10_000 DATA_UPLOAD_MAX_NUMBER_FIELDS = 10_000
@ -230,12 +184,8 @@ WAGTAILADMIN_BASE_URL = "http://example.com"
# see https://docs.wagtail.org/en/stable/advanced_topics/deploying.html#user-uploaded-files # see https://docs.wagtail.org/en/stable/advanced_topics/deploying.html#user-uploaded-files
WAGTAILDOCS_EXTENSIONS = ['csv', 'docx', 'key', 'odt', 'pdf', 'pptx', 'rtf', 'txt', 'xlsx', 'zip'] WAGTAILDOCS_EXTENSIONS = ['csv', 'docx', 'key', 'odt', 'pdf', 'pptx', 'rtf', 'txt', 'xlsx', 'zip']
CSRF_TRUSTED_ORIGINS = env_list( CSRF_TRUSTED_ORIGINS = [
"CSRF_TRUSTED_ORIGINS", 'https://innovedus-cms.fly.dev',
default=[] ]
)
ALLOWED_HOSTS = env_list( ALLOWED_HOSTS = ['innovedus-cms.fly.dev']
"ALLOWED_HOSTS",
default=[]
)

View File

@ -1,616 +0,0 @@
a {
text-decoration: none;
}
.site-container {
max-width: 890px;
margin: 0 auto;
padding: 0 16px;
}
.full-bleed {
width: 100vw;
margin-left: calc(50% - 50vw);
}
.site-header {
position: relative;
z-index: 10;
}
.template-darkbackground .site-header {
background-color: #0e1b42;
color: #ffffff;
}
.header-inner {
display: flex;
align-items: center;
gap: 24px;
padding: 30px 0;
}
.logo {
display: inline-flex;
align-items: center;
}
.logo--light {
display: none;
}
.logo--dark {
display: inline-flex;
}
.template-darkbackground .logo--light {
display: inline-flex;
}
.template-darkbackground .logo--dark {
display: none;
}
.main-nav {
flex: 1 1 auto;
margin-left: 16px;
top: 56px;
}
.main-menu {
display: flex;
align-items: center;
gap: 24px;
list-style: none;
margin: 0;
padding: 0;
width: 100%;
}
.menu-item {
position: relative;
flex: 1 1 0;
text-align: left;
}
.menu-item-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.submenu-toggle {
display: none;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: 0;
background: none;
padding: 0;
cursor: pointer;
color: inherit;
}
.submenu-toggle::after {
content: "";
width: 8px;
height: 8px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
display: block;
transition: transform 160ms ease;
}
.menu-item.is-open .submenu-toggle::after {
transform: rotate(225deg);
}
.menu-divider {
display: none;
height: 1px;
background: currentColor;
opacity: 0.6;
margin: 8px 0 0;
}
.menu-toggle {
display: none;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 4px;
width: 32px;
height: 32px;
background: none;
border: 0;
padding: 0;
cursor: pointer;
}
.menu-toggle-bar {
width: 20px;
height: 2px;
background: currentColor;
display: block;
}
.menu-toggle.is-open .menu-toggle-bar:nth-child(1) {
transform: translateY(6px) rotate(45deg);
}
.menu-toggle.is-open .menu-toggle-bar:nth-child(2) {
opacity: 0;
}
.menu-toggle.is-open .menu-toggle-bar:nth-child(3) {
transform: translateY(-6px) rotate(-45deg);
}
.main-menu-link {
display: inline-block;
padding: 12px 4px;
font-variation-settings: normal;
color: #0e1b42;
font-family: "Inter:Regular", "Noto Sans JP:Regular", sans-serif;
word-break: break-word;
font-weight: 400;
font-style: normal;
font-size: 14px;
letter-spacing: 0px;
line-height: normal;
--letter-spacing: 0px;
}
.template-darkbackground .main-menu-link {
color: #ffffff;
}
.submenu {
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
margin-top: -2px;
margin-left: 20px;
list-style: none;
padding-inline-start: 0;
border-bottom: #0e1b42;
border-style: solid;
border-width: 0 0 1px 0;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 160ms ease, transform 160ms ease;
}
.template-darkbackground .submenu {
border-bottom: #ffffff;
}
.menu-item:hover .submenu,
.menu-item:focus-within .submenu {
opacity: 1;
visibility: visible;
pointer-events: auto;
transform: translateX(-50%) translateY(2px);
}
.submenu-item {
min-width: 94px;
height: 36px;
border: #0e1b42;
border-style: solid;
border-width: 1px 1px 0 1px;
background-color: #ffffff;
}
.template-darkbackground .submenu-item {
border: #ffffff;
border-style: solid;
border-width: 1px 1px 0 1px;
background-color: #0e1b42;
}
.submenu-item a {
display: block;
font-variation-settings: normal;
color: #0e1b42;
font-family: "Inter:Regular", "Noto Sans JP:Regular", sans-serif;
word-break: break-word;
font-weight: 400;
font-style: normal;
font-size: 14px;
letter-spacing: 0px;
line-height: normal;
--letter-spacing: 0px;
padding: 8px 16px;
}
.template-darkbackground .submenu-item a {
color: #ffffff;
}
.submenu-item a:hover,
.submenu-item a:focus {
background: rgba(0, 0, 0, 0.05);
}
.header-search .search-input {
display: flex;
align-items: center;
}
.header-search {
margin-left: auto;
}
.header-search .search-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
background: none;
border: 0;
padding: 0;
cursor: pointer;
color: #1b2140;
}
.template-darkbackground .header-search .search-icon {
color: #ffffff;
}
.header-search .search-icon svg {
width: 28px;
height: 28px;
}
.header-search .search-input {
background-color: rgba(0, 0, 0, 0.2);
}
.template-darkbackground .header-search .search-input {
background-color: rgba(255, 255, 255, 0.4);
}
.header-search input[type="search"] {
border: 0;
background: transparent;
outline: none;
width: 181px;
}
.template-darkbackground .header-search input[type="search"] {
color: #ffffff;
}
.template-darkbackground .header-search input[type="search"]::placeholder {
color: #ffffff88;
}
footer {
background: #0e1b42;
color: #ffffff;
padding: 24px 0;
font-size: 14px;
}
.footer-inner {
display: flex;
justify-content: center;
gap: 48px;
align-items: flex-start;
text-align: left;
}
footer .company-info {
max-width: 300px;
margin: 0;
padding: 0 16px;
}
footer .copyright img {
margin: 10px 0;
width: 265px;
height: 37px;
}
footer .copyright p {
font-size: 10px;
text-align: left;
margin: 0;
}
.footer-socials {
display: flex;
gap: 12px;
margin: 16px 0;
flex-wrap: wrap;
}
.footer-socials .icon {
width: 32px;
height: 32px;
--fill-0: #ffffff;
}
.footer-socials .icon circle {
fill: #ffffff;
fill-opacity: 0.85;
}
.footer-socials .icon .icon-cutout {
fill: #0e1b42;
}
footer .footer-links {
padding: 0 16px;
}
footer .footer-links li {
list-style-type: none;
}
footer .footer-links a {
font-size: 14px;
color: #ffffff;
}
footer .footer-divider {
align-self: stretch;
display: flex;
align-items: center;
padding: 0 8px;
width: 1px;
border-left: 1px solid #ffffff;
}
footer .footer-sections {
display: flex;
gap: 32px;
flex-wrap: wrap;
}
@media (max-width: 1023px) {
.site-container {
max-width: 640px;
}
.header-search input[type="search"] {
width: 90px;
}
}
@media (max-width: 768px) {
.footer-inner {
flex-direction: column;
align-items: center;
text-align: center;
}
footer .company-info,
footer .footer-links {
max-width: 100%;
}
.footer-socials {
justify-content: center;
}
footer .footer-sections {
justify-content: center;
}
footer .footer-divider {
width: 100%;
padding: 12px 0;
border-left: 0;
border-top: 1px solid #ffffff;
}
footer .copyright p {
text-align: center;
}
}
@media (min-width: 575px) and (max-width: 767px) {
.site-container {
max-width: 426px;
}
.site-header .site-container {
padding: 0 44px;
margin: 0;
}
.main-menu {
justify-content: flex-end;
}
.header-search input[type="search"] {
width: 163px;
}
.main-nav {
right: -28px;
}
}
@media (max-width: 767px) {
.site-header .site-container {
position: relative;
}
.header-inner {
width: 100%;
}
.main-nav {
position: absolute;
width: 220px;
display: none;
padding: 16px 0;
background: #0e1b42;
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12);
z-index: 5;
}
.main-nav.is-open {
display: block;
}
.main-menu {
flex-direction: column;
align-items: flex-start;
gap: 12px;
width: 220px;
padding: 0 16px;
}
.menu-item {
flex: 0 0 auto;
width: 100%;
text-align: left;
}
.menu-item-header {
width: 192px;
}
.menu-toggle {
display: flex;
margin-left: 12px;
}
.template-darkbackground .menu-toggle {
color: #ffffff;
}
.main-nav {
background: #ffffff;
}
.template-darkbackground .main-nav {
background: #0e1b42;
}
.main-menu-link {
color: #0e1b42;
}
.template-darkbackground .main-menu-link {
color: #ffffff;
font-size: 14px;
}
.submenu-toggle {
display: inline-flex;
color: #0e1b42;
}
.template-darkbackground .submenu-toggle {
color: #ffffff;
}
.menu-divider {
margin-left: 0;
margin-right: 0;
width: 92px;
}
.menu-item.is-open .menu-divider {
display: block;
}
.submenu {
position: static;
margin: 8px 0 0;
opacity: 1;
visibility: visible;
pointer-events: auto;
display: none;
border: 0;
}
.menu-item.is-open .submenu {
display: block;
transform: none;
}
.submenu-item {
background: none;
border: 0;
height: auto;
}
.template-darkbackground .submenu-item {
border: 0;
height: auto;
}
.submenu-item a {
padding: 6px 0 6px 18px;
color: #0e1b42;
font-size: 14px;
}
.template-darkbackground .submenu-item a {
color: #ffffff;
}
}
@media (max-width: 574px) {
.site-container {
max-width: 300px;
}
.header-inner {
flex-wrap: wrap;
}
.header-search {
width: 100%;
order: 3;
margin-left: 0;
margin-top: 12px;
}
.header-search input[type="search"] {
width: 195px;
}
.main-nav {
right: 16px;
}
}
@layer figreset {
:root {
font-family: var( --default-font-family,ui-sans-serif,system-ui,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol','Noto Color Emoji' )
}
html {
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
-webkit-font-smoothing: antialiased;
width: 100%
}
body {
margin: 0;
width: 100%
}
body: has([data-page-overflowx='hidden']) {
overflow-x:hidden
}
body: has([data-page-overflowx='auto']) {
overflow-x:auto
}
}

View File

@ -1,37 +0,0 @@
document.addEventListener("DOMContentLoaded", function () {
var toggle = document.querySelector(".menu-toggle");
var nav = document.querySelector(".main-nav");
if (!toggle || !nav) {
return;
}
toggle.addEventListener("click", function () {
var isOpen = nav.classList.toggle("is-open");
toggle.classList.toggle("is-open", isOpen);
toggle.setAttribute("aria-expanded", isOpen ? "true" : "false");
});
var submenuToggles = document.querySelectorAll(".submenu-toggle");
submenuToggles.forEach(function (button) {
button.addEventListener("click", function () {
var item = button.closest(".menu-item");
if (!item) {
return;
}
var isOpen = item.classList.contains("is-open");
document.querySelectorAll(".menu-item.is-open").forEach(function (openItem) {
if (openItem !== item) {
openItem.classList.remove("is-open");
var openButton = openItem.querySelector(".submenu-toggle");
if (openButton) {
openButton.setAttribute("aria-expanded", "false");
}
}
});
item.classList.toggle("is-open", !isOpen);
button.setAttribute("aria-expanded", !isOpen ? "true" : "false");
});
});
});

View File

@ -34,21 +34,10 @@
<body class="{% block body_class %}{% endblock %}"> <body class="{% block body_class %}{% endblock %}">
{% wagtailuserbar %} {% wagtailuserbar %}
{% include "includes/header.html" %} {% block content %}{% endblock %}
<main class="site-main">
<div class="site-container">
{% block content %}{% endblock %}
</div>
</main>
{% include "includes/footer.html" %}
{# Global javascript #} {# Global javascript #}
<script type="text/javascript" src="{% static 'js/mysite.js' %}"></script> <script type="text/javascript" src="{% static 'js/mysite.js' %}"></script>
<script type="text/javascript" src="{% static 'js/header.js' %}"></script>
{# Instagram embed script to render IG oEmbeds #}
<script async src="https://www.instagram.com/embed.js"></script>
{% block extra_js %} {% block extra_js %}
{# Override this in templates to add extra javascript #} {# Override this in templates to add extra javascript #}

View File

@ -1,75 +0,0 @@
{% load navigation_tags %}
<footer>
<div class="site-container footer-inner">
<div class="company-info">
<div class="copyright">
{% get_footer_text %}
</div>
{% with social_links=settings.base.SocialMediaSettings.links %}
{% if social_links %}
<div class="footer-socials" aria-label="social icons">
{% for item in social_links %}
<a href="{{ item.value.url }}" target="_blank" aria-label="{{ item.value.platform }}">
{% if item.value.platform|lower == "facebook" %}
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 33.18 33.18" aria-label="facebook" 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)" id="Vector"/>
</svg>
{% elif item.value.platform|lower == "instagram" %}
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 33.18 33.18" aria-label="instagram" role="img">
<path d="M16.67 13.42C14.92 13.42 13.5 14.84 13.5 16.59C13.5 18.34 14.92 19.76 16.67 19.76C18.42 19.76 19.84 18.34 19.84 16.59C19.84 14.84 18.42 13.42 16.67 13.42Z" fill="var(--fill-0, #0E1B42)" id="Vector"/>
<path d="M20.6 8.42H12.57C10.28 8.42 8.42 10.28 8.42 12.57V20.6C8.42 22.89 10.28 24.75 12.57 24.75H20.6C22.89 24.75 24.75 22.89 24.75 20.6V12.57C24.75 10.28 22.89 8.42 20.6 8.42ZM16.67 21.54C13.94 21.54 11.72 19.32 11.72 16.59C11.72 13.86 13.94 11.64 16.67 11.64C19.4 11.64 21.62 13.86 21.62 16.59C21.62 19.32 19.4 21.54 16.67 21.54ZM21.91 12.48C21.26 12.48 20.73 11.95 20.73 11.3C20.73 10.65 21.26 10.12 21.91 10.12C22.56 10.12 23.09 10.65 23.09 11.3C23.09 11.95 22.56 12.48 21.91 12.48Z" fill="var(--fill-0, #0E1B42)" id="Vector_2"/>
<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 0ZM26.64 20.6C26.64 23.93 23.93 26.64 20.6 26.64H12.57C9.24 26.64 6.53 23.93 6.53 20.6V12.57C6.53 9.24 9.24 6.53 12.57 6.53H20.6C23.93 6.53 26.64 9.24 26.64 12.57V20.6Z" fill="var(--fill-0, #0E1B42)" id="Vector_3"/>
</svg>
{% elif item.value.platform|lower == "youtube" %}
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 33.18 33.18" aria-label="youtube" role="img">
<path d="M20.36 16.45L15.14 13.61C14.93 13.49 14.19 13.64 14.19 13.89V19.43C14.19 19.67 14.92 19.83 15.13 19.71L20.59 17.01C20.81 16.89 20.59 16.57 20.36 16.45Z" fill="var(--fill-0, #0E1B42)" id="Vector"/>
<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 0ZM27.74 19.42C27.74 22.05 25.61 24.18 22.98 24.18H10.81C8.18 24.18 6.05 22.05 6.05 19.42V13.76C6.05 11.13 8.18 9 10.81 9H22.98C25.61 9 27.74 11.13 27.74 13.76V19.42Z" fill="var(--fill-0, #0E1B42)" id="Vector_2"/>
</svg>
{% elif item.value.platform|lower == "threads" %}
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 33.18 33.18" aria-label="threads" 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>
{% elif item.value.platform|lower == "linkedin" %}
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 33.18 33.18" aria-label="linkedin" 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="M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854zm4.943 12.248V6.169H2.542v7.225zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248S2.4 3.226 2.4 3.934c0 .694.521 1.248 1.327 1.248zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016l.016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225z" transform="translate(7.59 7.59) scale(1.125)"/>
</svg>
{% elif item.value.platform|lower == "x" or item.value.platform|lower == "twitter" or item.value.platform|lower == "twitter-x" %}
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 33.18 33.18" aria-label="twitter-x" 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>
{% else %}
{{ item.value.platform }}
{% endif %}
</a>
{% endfor %}
</div>
{% endif %}
{% endwith %}
</div>
<div class="footer-divider" aria-hidden="true"></div>
<div class="footer-links">
{% if settings.base.NavigationSettings.footer_links %}
<div class="footer-sections">
{% for section in settings.base.NavigationSettings.footer_links %}
<div class="footer-section">
{% if section.value.title %}
<h3>{{ section.value.title }}</h3>
{% endif %}
<ul>
{% for link in section.value.links %}
<li><a href="{{ link.url }}" target="_blank">{{ link.label }}</a></li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
</footer>

View File

@ -1,140 +0,0 @@
{% load wagtailsettings_tags wagtailimages_tags %}
{% get_settings use_default_site=True as settings %}
<header class="site-header{% if settings.base.HeaderSettings.logo_light and settings.base.HeaderSettings.logo_dark %} has-logo-variants{% endif %}">
<div class="site-container">
<div class="header-inner">
{% if settings.base.HeaderSettings.logo_light and settings.base.HeaderSettings.logo_dark %}
<a href="/" class="logo logo--light">
{% if settings.base.HeaderSettings.site_name %}
{% image settings.base.HeaderSettings.logo_light fill-217x30 alt=settings.base.HeaderSettings.site_name %}
{% else %}
{% image settings.base.HeaderSettings.logo_light fill-217x30 %}
{% endif %}
</a>
<a href="/" class="logo logo--dark">
{% if settings.base.HeaderSettings.site_name %}
{% image settings.base.HeaderSettings.logo_dark fill-217x30 alt=settings.base.HeaderSettings.site_name %}
{% else %}
{% image settings.base.HeaderSettings.logo_dark fill-217x30 %}
{% endif %}
</a>
{% elif settings.base.HeaderSettings.logo_light %}
<a href="/" class="logo">
{% if settings.base.HeaderSettings.site_name %}
{% image settings.base.HeaderSettings.logo_light fill-217x30 alt=settings.base.HeaderSettings.site_name %}
{% else %}
{% image settings.base.HeaderSettings.logo_light fill-217x30 %}
{% endif %}
</a>
{% elif settings.base.HeaderSettings.logo_dark %}
<a href="/" class="logo">
{% if settings.base.HeaderSettings.site_name %}
{% image settings.base.HeaderSettings.logo_dark fill-217x30 alt=settings.base.HeaderSettings.site_name %}
{% else %}
{% image settings.base.HeaderSettings.logo_dark fill-217x30 %}
{% endif %}
</a>
{% endif %}
<nav class="main-nav" id="site-nav">
<ul class="main-menu" id="main-menu">
{% with site_root=page.get_site.root_page %}
{# Top-level menu: direct children of site root #}
<li class="menu-item">
<div class="menu-item-header">
<a href="#">
<span class="main-menu-link">最新文章</span>
</a>
{% if nav_latest_page or nav_trending_page %}
<button class="submenu-toggle" type="button" aria-expanded="false" aria-label="Toggle submenu"></button>
{% endif %}
</div>
{% if nav_latest_page or nav_trending_page %}
<span class="menu-divider" aria-hidden="true"></span>
<ul class="submenu">
{% if nav_latest_page %}
<li class="submenu-item">
<a href="{{ nav_latest_page.url }}">
<span class="submenu-item-link">{{ nav_latest_page.title }}</span>
</a>
</li>
{% endif %}
{% if nav_trending_page %}
<li class="submenu-item">
<a href="{{ nav_trending_page.url }}">
<span class="submenu-item-link">{{ nav_trending_page.title }}</span>
</a>
</li>
{% endif %}
</ul>
{% endif %}
</li>
{% for menu_page in site_root.get_children.live.in_menu %}
<li class="menu-item">
<div class="menu-item-header">
<a href="{{ menu_page.url }}">
<span class="main-menu-link">{{ menu_page.title }}</span>
</a>
{% with submenu=menu_page.get_children.live.in_menu %}
{% if submenu %}
<button class="submenu-toggle" type="button" aria-expanded="false" aria-label="Toggle submenu"></button>
{% endif %}
{% endwith %}
</div>
{# Second-level: direct children of each top-level page #}
{% with submenu=menu_page.get_children.live.in_menu %}
{% if submenu %}
<span class="menu-divider" aria-hidden="true"></span>
<ul class="submenu">
{% for subpage in submenu %}
<li class="submenu-item">
<a href="{{ subpage.url }}">
<span class="submenu-item-link">{{ subpage.title }}</span>
</a>
</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
</li>
{% endfor %}
{% endwith %}
{# Optional extra links from settings #}
{% if settings.base.HeaderSettings.main_links %}
{% for item in settings.base.HeaderSettings.main_links %}
<li class="menu-item">
<div class="menu-item-header">
<a href="{{ item.value.url }}">
<span class="main-menu-link">{{ item.value.label }}</span>
</a>
</div>
</li>
{% endfor %}
{% endif %}
</ul>
</nav>
<form class="header-search" action="{% url 'search' %}" method="get" role="search">
<div class="search-input">
<button type="submit" class="search-icon" aria-label="搜尋">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 4a6 6 0 104.24 10.24l4.76 4.76 1.42-1.42-4.76-4.76A6 6 0 0010 4zm0 2a4 4 0 110 8 4 4 0 010-8z"/></svg>
</button>
<input
type="search"
name="query"
placeholder="搜尋文章"
value="{{ request.GET.query|default:'' }}"
aria-label="搜尋文章">
</div>
</form>
<button class="menu-toggle" type="button" aria-expanded="false" aria-controls="site-nav" aria-label="Toggle menu">
<span class="menu-toggle-bar"></span>
<span class="menu-toggle-bar"></span>
<span class="menu-toggle-bar"></span>
</button>
</div>
</div>
</header>

View File

@ -7,14 +7,11 @@ from wagtail import urls as wagtail_urls
from wagtail.documents import urls as wagtaildocs_urls from wagtail.documents import urls as wagtaildocs_urls
from search import views as search_views from search import views as search_views
from home import views as home_views
urlpatterns = [ urlpatterns = [
path("django-admin/", admin.site.urls), path("django-admin/", admin.site.urls),
path("admin/", include(wagtailadmin_urls)), path("admin/", include(wagtailadmin_urls)),
path("documents/", include(wagtaildocs_urls)), path("documents/", include(wagtaildocs_urls)),
# use <str:slug> so Unicode tag slugs (e.g. 台北美食) still resolve
path("tags/<str:slug>/", home_views.hashtag_search, name="hashtag_search"),
path("search/", search_views.search, name="search"), path("search/", search_views.search, name="search"),
] ]

View File

@ -3,5 +3,3 @@ wagtail>=7.1,<7.2
gunicorn gunicorn
dj-database-url dj-database-url
psycopg[binary] psycopg[binary]
python-dotenv
django-storages[boto3]

View File

@ -1,38 +1,38 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load wagtailcore_tags %} {% load static wagtailcore_tags %}
{% block body_class %}template-searchresults{% endblock %} {% block body_class %}template-searchresults{% endblock %}
{% block title %} {% block title %}Search{% endblock %}
{% if search_query %}搜尋:{{ search_query }}{% else %}搜尋{% endif %}
{% endblock %}
{% block content %} {% block content %}
<nav class="breadcrumbs" aria-label="breadcrumb"> <h1>Search</h1>
<ol>
<li>
{% if site_root %}
<a href="{{ site_root.url }}">首頁</a>
{% else %}
<a href="/">首頁</a>
{% endif %}
</li>
<li><span>搜尋</span></li>
{% if search_query %}
<li><span>{{ search_query }}</span></li>
{% endif %}
</ol>
</nav>
<section class="search-results"> <form action="{% url 'search' %}" method="get">
{% if search_query %} <input type="text" name="query"{% if search_query %} value="{{ search_query }}"{% endif %}>
{% if results_count %} <input type="submit" value="Search" class="button">
{% include "home/includes/page-article-list.html" %} </form>
{% else %}
<p>找不到與「{{ search_query }}」相關的文章。</p> {% if search_results %}
{% endif %} <ul>
{% else %} {% for result in search_results %}
<p>請輸入關鍵字後再進行搜尋。</p> <li>
{% endif %} <h4><a href="{% pageurl result %}">{{ result }}</a></h4>
</section> {% if result.search_description %}
{{ result.search_description }}
{% endif %}
</li>
{% endfor %}
</ul>
{% if search_results.has_previous %}
<a href="{% url 'search' %}?query={{ search_query|urlencode }}&amp;page={{ search_results.previous_page_number }}">Previous</a>
{% endif %}
{% if search_results.has_next %}
<a href="{% url 'search' %}?query={{ search_query|urlencode }}&amp;page={{ search_results.next_page_number }}">Next</a>
{% endif %}
{% elif search_query %}
No results found
{% endif %}
{% endblock %} {% endblock %}

View File

@ -1,54 +1,46 @@
from urllib.parse import urlencode from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.core.paginator import Paginator
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.db.models import Q
from wagtail.models import Site from wagtail.models import Page
from home.models import ArticlePage, PAGE_SIZE # To enable logging of search queries for use with the "Promoted search results" module
# <https://docs.wagtail.org/en/stable/reference/contrib/searchpromotions.html>
# uncomment the following line and the lines indicated in the search function
# (after adding wagtail.contrib.search_promotions to INSTALLED_APPS):
# from wagtail.contrib.search_promotions.models import Query
def search(request): def search(request):
search_query = (request.GET.get("query") or "").strip() search_query = request.GET.get("query", None)
page_number = request.GET.get("page", 1) page = request.GET.get("page", 1)
category_sections = []
results_page = None
results_count = 0
# Search
if search_query: if search_query:
primary_qs = ArticlePage.objects.live().search(search_query) search_results = Page.objects.live().search(search_query)
results_count = primary_qs.count()
if not results_count: # To log this query for use with the "Promoted search results" module:
fallback_filter = Q(intro__icontains=search_query) | Q(body__icontains=search_query)
primary_qs = ArticlePage.objects.live().filter(fallback_filter).order_by("-date")
results_count = primary_qs.count()
if results_count: # query = Query.get(search_query)
paginator = Paginator(primary_qs, PAGE_SIZE) # query.add_hit()
results_page = paginator.get_page(page_number)
query_string = urlencode({"query": search_query})
category_sections = [
{
"title": f"搜尋:{search_query}",
"items": results_page,
"url": f"{request.path}?{query_string}",
}
]
site = Site.find_for_request(request) else:
site_root = site.root_page if site else None search_results = Page.objects.none()
# Pagination
paginator = Paginator(search_results, 10)
try:
search_results = paginator.page(page)
except PageNotAnInteger:
search_results = paginator.page(1)
except EmptyPage:
search_results = paginator.page(paginator.num_pages)
return TemplateResponse( return TemplateResponse(
request, request,
"search/search.html", "search/search.html",
{ {
"search_query": search_query, "search_query": search_query,
"category_sections": category_sections, "search_results": search_results,
"results_page": results_page,
"results_count": results_count,
"site_root": site_root,
"page": site_root.specific if site_root else None,
}, },
) )